Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-06 14:33:03 +03:00
commit 08f039da64
766 changed files with 10526 additions and 4558 deletions

View file

@ -80,6 +80,7 @@ configurations.androidTestImplementation {
dependencies {
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.account)
implementation(projects.domain.models)
implementation(projects.domain.core)
implementation(projects.domain.card)
@ -232,6 +233,10 @@ dependencies {
implementation(projects.features.welcome.impl)
implementation(projects.features.createWalletSelection.api)
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)
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)

View file

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

View file

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

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

@ -0,0 +1,96 @@
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
*/
fun setWireMockScenarioState(
scenarioName: String,
state: String,
baseUrl: String = "[REDACTED_ENV_URL]"
): Boolean {
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
}
}

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

View file

@ -3,9 +3,12 @@ package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
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.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class DisclaimerPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<DisclaimerPageObject>(
@ -13,6 +16,15 @@ class DisclaimerPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
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 {
hasTestTag(DisclaimerScreenTestTags.ACCEPT_BUTTON)
}

View file

@ -39,6 +39,11 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
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
*/

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,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,488 @@
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.resetWireMockScenarios
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() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarios()
}
).run {
val tokenTitle = "Bitcoin"
val balance = TOTAL_BALANCE
resetWireMockScenarios()
step("Setup WireMock scenario for 'Error' state") {
setWireMockScenarioState("payment_methods", "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"
resetWireMockScenarios()
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"
resetWireMockScenarios()
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"
resetWireMockScenarios()
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%"
resetWireMockScenarios()
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 unavailable provider name is displayed") {
onSelectProviderBottomSheet {
flakySafely(timeoutMs = 20_000) {
unavailableProviderItem.assertIsDisplayed()
}
}
}
step("Assert available provider name is displayed") {
onSelectProviderBottomSheet {
flakySafely(timeoutMs = 20_000) {
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"
resetWireMockScenarios()
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

@ -61,7 +61,7 @@ class DetailsTest : BaseTestCase() {
}
}
@Test
// @Test
fun wallet2DetailsTest() =
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule, ProductType.Wallet2))

View file

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

View file

@ -2,7 +2,9 @@ package com.tangem.tests
import androidx.compose.ui.test.onAllNodesWithText
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeUp
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.onMainScreen
import com.tangem.screens.onOrganizeTokensScreen
@ -27,6 +29,9 @@ class OrganizeTokensTest : BaseTestCase() {
step("Click on 'Synchronize addresses' button" ) {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
}
@ -48,6 +53,9 @@ class OrganizeTokensTest : BaseTestCase() {
step("Assert tokens were grouped on 'Main screen'") {
onMainScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsDisplayed() }
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
}
@ -79,18 +87,25 @@ class OrganizeTokensTest : BaseTestCase() {
setupHooks().run {
val ethereumTitle = "Ethereum"
val bitcoinTitle = "Bitcoin"
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("Check positions of tokens on 'Main Screen'") {
onMainScreen {
tokenWithTitleAndPosition(bitcoinTitle, 0).assertIsDisplayed()
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
}
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
}
@ -123,6 +138,9 @@ class OrganizeTokensTest : BaseTestCase() {
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
}
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
}
@ -154,12 +172,17 @@ class OrganizeTokensTest : BaseTestCase() {
val ethereumTitle = "Ethereum"
val bitcoinTitle = "Bitcoin"
val polygonTitle = "Polygon"
val polExMaticTitle = "POL (ex-MATIC)"
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("Check positions of tokens on 'Main Screen'") {
onMainScreen {
tokenWithTitleAndPosition(bitcoinTitle, 0).assertIsDisplayed()
@ -167,6 +190,9 @@ class OrganizeTokensTest : BaseTestCase() {
tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed()
}
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
}
@ -175,6 +201,7 @@ class OrganizeTokensTest : BaseTestCase() {
tokenWithTitleAndPosition(bitcoinTitle, 1).assertIsDisplayed()
tokenWithTitleAndPosition(ethereumTitle, 2).assertIsDisplayed()
tokenWithTitleAndPosition(polygonTitle, 3).assertIsDisplayed()
tokenWithTitleAndPosition(polExMaticTitle, 4).assertIsDisplayed()
}
}
step("Click 'By Balance' button") {
@ -185,8 +212,9 @@ class OrganizeTokensTest : BaseTestCase() {
step("Check positions of tokens by balance on 'Organize tokens' screen") {
onOrganizeTokensScreen {
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed()
tokenWithTitleAndPosition(bitcoinTitle, 3).assertIsDisplayed()
tokenWithTitleAndPosition(polExMaticTitle, 2).assertIsDisplayed()
tokenWithTitleAndPosition(polygonTitle, 3).assertIsDisplayed()
tokenWithTitleAndPosition(bitcoinTitle, 4).assertIsDisplayed()
}
}
step("Click 'Apply' button") {
@ -195,8 +223,9 @@ class OrganizeTokensTest : BaseTestCase() {
step("Check positions of tokens by balance on 'Organize tokens' screen") {
onMainScreen {
tokenWithTitleAndPosition(ethereumTitle, 0).assertIsDisplayed()
tokenWithTitleAndPosition(polygonTitle, 1).assertIsDisplayed()
tokenWithTitleAndPosition(bitcoinTitle, 2).assertIsDisplayed()
tokenWithTitleAndPosition(polExMaticTitle, 1).assertIsDisplayed()
tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed()
tokenWithTitleAndPosition(bitcoinTitle, 3).assertIsDisplayed()
}
}
}

View file

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

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 e87768263c0a79018958b0872940c604180a623a

View file

@ -6,6 +6,7 @@ import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.WindowManager
import androidx.activity.SystemBarStyle
@ -175,13 +176,22 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
internal lateinit var testerMenuLauncher: TesterMenuLauncher
@Inject
internal lateinit var intentProcessor: IntentProcessor
@Inject
internal lateinit var walletConnectLinkIntentHandler: WalletConnectLinkIntentHandler
@Inject
internal lateinit var onPushClickedIntentHandler: OnPushClickedIntentHandler
@Inject
internal lateinit var backgroundScanIntentHandler: BackgroundScanIntentHandler
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
// TODO: fixme: inject through DI
private val intentProcessor: IntentProcessor = IntentProcessor()
private val dialogManager = DialogManager()
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
@ -231,7 +241,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
lifecycle.addObserver(defaultDeviceFlipDetector)
if (BuildConfig.TESTER_MENU_ENABLED) {
lifecycle.addObserver(testerMenuLauncher.launchOnShakeObserver)
lifecycle.addObserver(testerMenuLauncher.launchOnKeyEventObserver)
}
}
@ -343,12 +353,10 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
}
private fun initIntentHandlers() {
val hasSavedWalletsProvider = { userWalletsListManager.hasUserWallets }
intentProcessor.addHandler(OnPushClickedIntentHandler(analyticsEventsHandler))
intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope))
intentProcessor.addHandler(onPushClickedIntentHandler)
if (!walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
intentProcessor.addHandler(WalletConnectLinkIntentHandler())
intentProcessor.addHandler(walletConnectLinkIntentHandler)
}
}
@ -409,10 +417,17 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
val result = WindowObscurationObserver.dispatchTouchEvent(event, analyticsEventsHandler)
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?) {
val backStack = appRouterConfig.stack ?: emptyList()
// TODO move inital navigation to navigation component ([REDACTED_JIRA])
@ -434,9 +449,15 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
}
private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) {
val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity)
if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) {
store.dispatchNavigationAction {
replaceAll(AppRoute.Welcome(intentWhichStartedActivity?.let(::SerializableIntent)))
replaceAll(
AppRoute.Welcome(
launchMode = launchMode,
intent = intentWhichStartedActivity?.let(::SerializableIntent),
),
)
}
intentProcessor.handleIntent(
intent = intentWhichStartedActivity,
@ -450,7 +471,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
val route = if (shouldShowTos) {
AppRoute.Disclaimer(isTosAccepted = false)
} else {
AppRoute.Home
AppRoute.Home(launchMode = launchMode)
}
store.dispatchNavigationAction { replaceAll(route) }

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

@ -6,9 +6,9 @@ import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.features.home.impl.analytics.IntroductionProcess
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
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.features.demo.DemoHelper
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) {
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) {
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.features.details.redux.DetailsReducer
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.proxy.redux.DaggerGraphReducer
import org.rekotlin.Action
@ -14,7 +13,6 @@ fun appReducer(action: Action, state: AppState?): AppState {
return AppState(
globalState = globalReducer(action, state),
homeState = HomeReducer.reduce(action, state),
detailsState = DetailsReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
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.walletconnect.WalletConnectMiddleware
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.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
@ -20,7 +18,6 @@ import org.rekotlin.StateType
data class AppState(
val globalState: GlobalState = GlobalState(),
val homeState: HomeState = HomeState(),
val detailsState: DetailsState = DetailsState(),
val walletConnectState: WalletConnectState = WalletConnectState(),
val welcomeState: WelcomeState = WelcomeState(),
@ -32,7 +29,6 @@ data class AppState(
return listOf(
logMiddleware,
GlobalMiddleware.handler,
HomeMiddleware.handler,
DetailsMiddleware().detailsMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.compose.ui.text.intl.Locale
import androidx.core.view.isVisible
import com.tangem.core.analytics.Analytics
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.dispatchOpenUrl
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.scope
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_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 {
return AlertDialog.Builder(context, R.style.CustomMaterialDialog).apply {
@ -62,8 +62,8 @@ internal object ScanFailsDialog {
source = sourceAnalytics,
),
)
val locale = LocaleRegionProvider().getRegion()
val link = if (locale.lowercase() == RUSSIA_COUNTRY_CODE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK
val locale = Locale.current.region
val link = if (locale.lowercase() == RUSSIA_LOCALE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK
store.dispatchOpenUrl(link)
}
customView.findViewById<TextView>(R.id.request_support_button)?.setOnClickListener {

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

@ -0,0 +1,40 @@
package com.tangem.tap.di.domain
import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase
import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase
import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase
import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase
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(): AddCryptoPortfolioUseCase {
return AddCryptoPortfolioUseCase()
}
@Provides
@Singleton
fun provideUpdateCryptoPortfolioUseCase(): UpdateCryptoPortfolioUseCase {
return UpdateCryptoPortfolioUseCase()
}
@Provides
@Singleton
fun provideArchiveCryptoPortfolioUseCase(): ArchiveCryptoPortfolioUseCase {
return ArchiveCryptoPortfolioUseCase()
}
@Provides
@Singleton
fun provideRecoverCryptoPortfolioUseCase(): RecoverCryptoPortfolioUseCase {
return RecoverCryptoPortfolioUseCase()
}
}

View file

@ -2,11 +2,14 @@ package com.tangem.tap.di.domain
import com.tangem.domain.card.*
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.IsDemoCardUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
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.NetworkHasDerivationUseCase
import com.tangem.sdk.api.TangemSdkManager

View file

@ -1,11 +1,12 @@
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.repository.CustomTokensRepository
import com.tangem.domain.managetokens.repository.ManageTokensRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -72,6 +73,7 @@ internal object ManageTokensDomainModule {
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): SaveManagedTokensUseCase {
return SaveManagedTokensUseCase(
customTokensRepository = customTokensRepository,
@ -81,6 +83,7 @@ internal object ManageTokensDomainModule {
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
)
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.di.domain
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.repositories.MarketsTokenRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
@ -9,6 +9,7 @@ import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
@ -63,6 +64,7 @@ object MarketsDomainModule {
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): SaveMarketTokensUseCase {
return SaveMarketTokensUseCase(
derivationsRepository = derivationsRepository,
@ -71,6 +73,7 @@ object MarketsDomainModule {
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
)
}

View file

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

View file

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

View file

@ -11,7 +11,9 @@ import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
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.MultiYieldBalanceSupplier
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
@ -46,6 +48,7 @@ internal object TokensDomainModule {
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
stakingIdFactory: StakingIdFactory,
): AddCryptoCurrenciesUseCase {
return AddCryptoCurrenciesUseCase(
currenciesRepository = currenciesRepository,
@ -54,6 +57,7 @@ internal object TokensDomainModule {
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
stakingIdFactory = stakingIdFactory,
)
}
@ -64,12 +68,14 @@ internal object TokensDomainModule {
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): FetchTokenListUseCase {
return FetchTokenListUseCase(
currenciesRepository = currenciesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
)
}
@ -172,6 +178,7 @@ internal object TokensDomainModule {
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
stakingIdFactory: StakingIdFactory,
): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase(
currenciesRepository = currenciesRepository,
@ -180,6 +187,7 @@ internal object TokensDomainModule {
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
stakingIdFactory = stakingIdFactory,
)
}
@ -190,12 +198,14 @@ internal object TokensDomainModule {
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): FetchCardTokenListUseCase {
return FetchCardTokenListUseCase(
currenciesRepository = currenciesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
)
}
@ -395,7 +405,6 @@ internal object TokensDomainModule {
tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
@ -403,13 +412,14 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceSupplier: MultiYieldBalanceSupplier,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory,
): BaseCurrenciesStatusesOperations {
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
@ -417,9 +427,11 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceSupplier = multiYieldBalanceSupplier,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
stakingIdFactory = stakingIdFactory,
)
}
@ -429,7 +441,6 @@ internal object TokensDomainModule {
tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
@ -437,13 +448,14 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceSupplier: MultiYieldBalanceSupplier,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory,
): BaseCurrencyStatusOperations {
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
@ -451,9 +463,11 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceSupplier = multiYieldBalanceSupplier,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
stakingIdFactory = stakingIdFactory,
)
}
@ -472,6 +486,7 @@ internal object TokensDomainModule {
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
): WalletBalanceFetcher {
return WalletBalanceFetcher(
@ -481,6 +496,7 @@ internal object TokensDomainModule {
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
dispatchers = dispatchers,
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.di.domain
import com.tangem.data.wallets.hot.TangemHotWalletSigner
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.demo.models.DemoConfig
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.usecase.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.tap.domain.hot.TangemHotWalletSigner
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -180,8 +180,13 @@ internal object TransactionDomainModule {
fun providePrepareForSendUseCase(
transactionRepository: TransactionRepository,
cardSdkConfigRepository: CardSdkConfigRepository,
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
): PrepareForSendUseCase {
return PrepareForSendUseCase(transactionRepository, cardSdkConfigRepository)
return PrepareForSendUseCase(
transactionRepository = transactionRepository,
cardSdkConfigRepository = cardSdkConfigRepository,
getHotTransactionSigner = { tangemHotWalletSignerFactory.create(it) },
)
}
@Provides
@ -189,8 +194,13 @@ internal object TransactionDomainModule {
fun provideSignUseCase(
walletManagersFacade: WalletManagersFacade,
cardSdkConfigRepository: CardSdkConfigRepository,
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
): SignUseCase {
return SignUseCase(cardSdkConfigRepository, walletManagersFacade)
return SignUseCase(
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
getHotTransactionSigner = { tangemHotWalletSignerFactory.create(it) },
)
}
@Provides

View file

@ -1,8 +1,6 @@
package com.tangem.tap.di.hot
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 dagger.Binds
import dagger.Module
@ -17,8 +15,4 @@ internal interface TangemHotSdkModule {
@Binds
@Singleton
fun bindTangemHotSdk(proxy: TangemHotSDKProxy): TangemHotSdk
@Binds
@Singleton
fun bindHotWalletPasswordRequester(impl: DefaultHotWalletPasswordRequester): HotWalletPasswordRequester
}

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,7 +20,7 @@ import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.common.util.derivationStyleProvider
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId

View file

@ -14,7 +14,7 @@ import com.tangem.common.map
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.card.CardTypesResolver
import com.tangem.domain.card.DerivationStyleProvider
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.domain.card.common.TapWorkarounds.isTestCard
import com.tangem.domain.card.configs.CardConfig
import com.tangem.domain.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.crypto.hdWallet.DerivationPath
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.models.scan.CardDTO
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.crypto.CryptoUtils
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.isNotSupportedInThatRelease
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.card.common.TapWorkarounds.isVisa
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.configs.CardConfig
import com.tangem.domain.models.scan.CardDTO

View file

@ -16,7 +16,7 @@ import com.tangem.common.extensions.toHexString
import com.tangem.core.error.ext.tangemError
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.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.VisaWalletPublicKeyUtility
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation

View file

@ -233,7 +233,7 @@ class DetailsMiddleware {
deleteSavedAccessCodes()
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false)
store.dispatchNavigationAction { replaceAll(AppRoute.Home) }
store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
return CompletionResult.Success(Unit)
}

View file

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

View file

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

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

View file

@ -4,8 +4,8 @@ import android.content.Intent
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.removePrefixOrNull
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.features.intentHandler.AffectsNavigation
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.store
import timber.log.Timber
import java.net.URLDecoder

View file

@ -24,8 +24,6 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.common.LogConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.notifications.GetApplicationIdUseCase
import com.tangem.domain.notifications.SendPushTokenUseCase
import com.tangem.domain.notifications.models.ApplicationId
@ -212,15 +210,11 @@ internal class MainViewModel @Inject constructor(
}
private fun makeSellExchangeService(environmentConfig: EnvironmentConfig): ExchangeService {
val cardProvider: () -> ScanResponse? = {
userWalletsListManager.selectedUserWalletSync?.requireColdWallet()?.scanResponse // TODO [REDACTED_TASK_KEY]
}
return MoonPayService(
apiKey = environmentConfig.moonPayApiKey,
secretKey = environmentConfig.moonPayApiSecretKey,
logEnabled = LogConfig.network.moonPayService,
cardProvider = { cardProvider.invoke()?.card },
userWalletProvider = { userWalletsListManager.selectedUserWalletSync },
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.welcome.component
import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
@ -7,6 +8,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
interface WelcomeComponent : ComposableContentComponent {
data class Params(
val launchMode: InitScreenLaunchMode,
val intent: SerializableIntent?,
)

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.welcome.model
import com.tangem.common.core.TangemError
import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.core.analytics.Analytics
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
@ -44,10 +45,9 @@ internal class WelcomeModel @Inject constructor(
subscribeToStoreChanges()
initGlobalState()
val welcomeAction = if (params.intent != null) {
WelcomeAction.ProceedWithIntent(params.intent.toIntent())
} else {
WelcomeAction.ProceedWithBiometrics()
val welcomeAction = when (params.launchMode) {
is InitScreenLaunchMode.WithCardScan -> WelcomeAction.ProceedWithCard
is InitScreenLaunchMode.Standard -> WelcomeAction.ProceedWithBiometrics(params.intent?.toIntent())
}
store.dispatch(welcomeAction)

View file

@ -20,10 +20,8 @@ import com.tangem.tap.*
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.proxy.redux.DaggerGraphState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
@ -44,7 +42,7 @@ internal class WelcomeMiddleware {
private fun handleAction(action: WelcomeAction, state: WelcomeState) {
mainScope.launch {
when (action) {
is WelcomeAction.ProceedWithIntent -> proceedWithIntent(action.intent, scope = this)
is WelcomeAction.ProceedWithIntent -> proceedWithIntent(action.intent)
is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics(
afterUnlockIntent = action.afterUnlockIntent ?: state.intent,
)
@ -55,7 +53,7 @@ internal class WelcomeMiddleware {
}
}
private suspend fun proceedWithIntent(initialIntent: Intent, scope: CoroutineScope) {
private suspend fun proceedWithIntent(initialIntent: Intent) {
Timber.d(
"""
Proceeding with intent
@ -63,15 +61,12 @@ internal class WelcomeMiddleware {
""".trimIndent(),
)
val handler = BackgroundScanIntentHandler(
scope = scope,
hasSavedUserWalletsProvider = { true },
)
val isBackgroundScanHandled = handler.handleIntent(initialIntent, isFromForeground = false)
val hasUncompletedBackup = backupService.hasIncompletedBackup
if (!isBackgroundScanHandled && !hasUncompletedBackup) {
if (!hasUncompletedBackup) {
store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics(initialIntent))
} else {
store.dispatchWithMain(WelcomeAction.ProceedWithCard)
}
}
@ -139,7 +134,7 @@ internal class WelcomeMiddleware {
}
private fun sendSignedInAnalyticsEvent(userWallet: UserWallet, signInType: Basic.SignedIn.SignInType) {
// TODO [REDACTED_TASK_KEY]
// TODO [REDACTED_TASK_KEY] [Hot Wallet] Analytics
if (userWallet !is UserWallet.Cold) {
return

View file

@ -13,9 +13,9 @@ import com.tangem.domain.core.lce.Lce
import com.tangem.domain.exchange.ExpressAvailabilityState
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.utils.Provider

View file

@ -12,7 +12,7 @@ import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.ExchangeServiceInitializationStatus
@ -27,7 +27,7 @@ class MoonPayService(
private val apiKey: String,
private val secretKey: String,
private val logEnabled: Boolean,
private val cardProvider: () -> CardDTO?,
private val userWalletProvider: () -> UserWallet?,
) : ExchangeService {
override val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
@ -103,8 +103,8 @@ class MoonPayService(
}
override fun availableForSell(currency: Currency): Boolean {
val card = cardProvider() ?: return false
val checkCardExchange = !card.isStart2Coin
val userWallet = userWalletProvider() ?: return false
val checkCardExchange = userWallet !is UserWallet.Cold || !userWallet.scanResponse.card.isStart2Coin
if (!checkCardExchange) return false

View file

@ -14,6 +14,7 @@ import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
import com.tangem.features.hotwallet.AddExistingWalletComponent
import com.tangem.features.hotwallet.CreateMobileWalletComponent
import com.tangem.features.hotwallet.WalletActivationComponent
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensSource
@ -42,7 +43,7 @@ import com.tangem.tap.features.details.ui.cardsettings.coderecovery.api.AccessCo
import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent
import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent
import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent
import com.tangem.tap.features.home.api.HomeComponent
import com.tangem.features.home.api.HomeComponent
import com.tangem.tap.features.welcome.component.WelcomeComponent
import com.tangem.tap.routing.component.RoutingComponent.Child
import dagger.hilt.android.scopes.ActivityScoped
@ -91,6 +92,7 @@ internal class ChildFactory @Inject constructor(
private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory,
private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory,
private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory,
private val walletActivationComponentFactory: WalletActivationComponent.Factory,
private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory,
private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
@ -133,6 +135,7 @@ internal class ChildFactory @Inject constructor(
createComponentChild(
context = context,
params = WelcomeComponent.Params(
launchMode = route.launchMode,
intent = route.intent,
),
componentFactory = welcomeComponentFactory,
@ -290,7 +293,7 @@ internal class ChildFactory @Inject constructor(
is AppRoute.Home -> {
createComponentChild(
context = context,
params = Unit,
params = HomeComponent.Params(route.launchMode),
componentFactory = homeComponentFactory,
)
}
@ -384,6 +387,7 @@ internal class ChildFactory @Inject constructor(
context = context,
params = PushNotificationsParams(
modelCallbacks = PushNotificationsModelCallbacksStub(),
nextRoute = AppRoute.Home(),
),
componentFactory = pushNotificationsComponentFactory,
)
@ -437,6 +441,7 @@ internal class ChildFactory @Inject constructor(
initialCurrency = route.initialCurrency,
selectedCurrency = route.selectedCurrency,
source = ChooseManagedTokensComponent.Source.valueOf(route.source.name),
showSendViaSwapNotification = route.showSendViaSwapNotification,
),
componentFactory = chooseManagedTokensComponentFactory,
)
@ -462,6 +467,15 @@ internal class ChildFactory @Inject constructor(
componentFactory = addExistingWalletComponentFactory,
)
}
is AppRoute.WalletActivation -> {
createComponentChild(
context = context,
params = WalletActivationComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = walletActivationComponentFactory,
)
}
is AppRoute.SendEntryPoint -> {
createComponentChild(
context = context,

View file

@ -3,6 +3,7 @@ package com.tangem.common.routing
import android.os.Bundle
import com.tangem.common.routing.bundle.RouteBundleParams
import com.tangem.common.routing.bundle.bundle
import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.appcurrency.model.AppCurrency
@ -22,10 +23,14 @@ sealed class AppRoute(val path: String) : Route {
data object Initial : AppRoute(path = "/initial")
@Serializable
data object Home : AppRoute(path = "/home")
data class Home(
val launchMode: InitScreenLaunchMode = InitScreenLaunchMode.Standard,
) : AppRoute(path = "/home")
@Serializable
data class Welcome(
val launchMode: InitScreenLaunchMode = InitScreenLaunchMode.Standard,
// we still have this param to be handled by WalletConnectLinkIntentHandler in WelcomeMiddleware
val intent: SerializableIntent? = null,
) : AppRoute(path = "/welcome"), RouteBundleParams {
@ -128,6 +133,7 @@ sealed class AppRoute(val path: String) : Route {
val initialCurrency: CryptoCurrency,
val selectedCurrency: CryptoCurrency?,
val source: Source,
val showSendViaSwapNotification: Boolean,
) : AppRoute(path = "/$source/choose_managed_tokens/$userWalletId/${initialCurrency.id.value}") {
enum class Source {
SendViaSwap,
@ -295,6 +301,11 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
object AddExistingWallet : AppRoute(path = "/add_existing_wallet")
@Serializable
data class WalletActivation(
val userWalletId: UserWalletId,
) : AppRoute(path = "/wallet_activation/${userWalletId.stringValue}")
@Serializable
data class SendEntryPoint(
val userWalletId: UserWalletId,

View file

@ -0,0 +1,13 @@
package com.tangem.common.routing.entity
import kotlinx.serialization.Serializable
@Serializable
sealed class InitScreenLaunchMode {
@Serializable
data object Standard : InitScreenLaunchMode()
@Serializable
data object WithCardScan : InitScreenLaunchMode()
}

View file

@ -14,6 +14,7 @@ data class SerializableIntent(
val packageValue: String?,
val component: String?,
val flags: Int,
// CAUTION: works wrong with SerializableBundle constructor(bundle: Bundle), need to be removed
val extras: SerializableBundle?,
) {

View file

@ -5,7 +5,7 @@ import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.domain.staking.model.StakingID
import com.tangem.domain.models.staking.StakingID
import java.math.BigDecimal
/**

View file

@ -4,7 +4,7 @@ import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentD
import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
import com.tangem.domain.staking.model.StakingID
import com.tangem.domain.models.staking.StakingID
import java.math.BigDecimal
/**

View file

@ -8,8 +8,8 @@ import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.common.test.domain.card.MockScanResponseFactory
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.domain.card.DerivationStyleProvider
import com.tangem.domain.card.common.util.derivationStyleProvider
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.card.configs.GenericCardConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
@ -25,6 +25,7 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul
val cardano by lazy { createCoin(blockchain = Blockchain.Cardano) }
val chia by lazy { createCoin(Blockchain.Chia) }
val ethereum by lazy { createCoin(Blockchain.Ethereum) }
val stellar by lazy { createCoin(Blockchain.Stellar) }
val chiaAndEthereum by lazy {
listOf(
@ -64,6 +65,10 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul
hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource,
canHandleTokens = false,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = when (blockchain) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.NameResolvingType.ENS
else -> Network.NameResolvingType.NONE
},
)
return factory.createCoin(network = network)
@ -94,6 +99,7 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul
hasFiatFeeRate = true,
canHandleTokens = true,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.NONE,
),
name = "NEVER-MIND",
symbol = "NEVER-MIND",

View file

@ -9,7 +9,24 @@ fun <B> assertEither(actual: Either<Throwable, B>, expected: Either<Throwable, B
.onLeft {
val expectedError = expected.leftOrNull() ?: error("Actual is Either.Left: $it")
Truth.assertThat(it::class.java).isEqualTo(expectedError::class.java)
Truth.assertThat(it).isInstanceOf(expectedError::class.java)
Truth.assertThat(it).hasMessageThat().isEqualTo(expectedError.message)
}
}
fun assertEitherRight(actual: Either<Throwable, Unit>) {
actual
.onRight { Truth.assertThat(actual).isEqualTo(Either.Right(Unit)) }
.onLeft {
error("Actual is Either.Left: $it")
}
}
fun <B> assertEitherLeft(actual: Either<Throwable, B>, expected: Throwable) {
actual
.onRight { error("Actual is Either.Right: $it") }
.onLeft {
Truth.assertThat(it::class.java).isEqualTo(expected::class.java)
Truth.assertThat(it).hasMessageThat().isEqualTo(expected.message)
}
}

View file

@ -4,7 +4,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.isNullOrZero
import com.tangem.utils.transformer.Transformer

View file

@ -14,7 +14,7 @@ import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.isNullOrZero
import com.tangem.utils.transformer.Transformer
import java.math.BigDecimal

View file

@ -15,7 +15,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.isNullOrZero
import com.tangem.utils.transformer.Transformer
import java.math.BigDecimal

View file

@ -16,7 +16,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero

View file

@ -1,7 +1,7 @@
package com.tangem.common.ui.amountScreen.converters
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.converter.Converter
/**

View file

@ -9,7 +9,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.transformer.Transformer
/**

View file

@ -16,7 +16,7 @@ import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.isNullOrZero
import com.tangem.utils.transformer.Transformer
import java.math.BigDecimal

View file

@ -9,9 +9,9 @@ import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.AmountType
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.convertToAmount
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter

View file

@ -12,7 +12,7 @@ import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.extensions.isZero
import com.tangem.utils.transformer.Transformer
import java.math.BigDecimal

View file

@ -43,9 +43,9 @@ fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) {
config = config,
containerColor = TangemTheme.colors.background.secondary,
titleText = resourceReference(R.string.give_permission_title),
titleAction = TopAppBarButtonUM(
titleAction = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_information_24,
onIconClicked = { isPermissionAlertShow = true },
onClicked = { isPermissionAlertShow = true },
),
content = { content: GiveTxPermissionBottomSheetConfig ->
GiveTxPermissionBottomSheetContent(content = content)

View file

@ -49,10 +49,10 @@ fun SendingText(footerText: TextReference, modifier: Modifier = Modifier) {
text = footerText.resolveAnnotatedReference(),
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.primary1,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
.padding(16.dp),
)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.common.ui.notifications
/**
* NotificationId represents unique identifiers for notifications in the app.
*
* These ids can be used with [ShouldShowNotificationUseCase] and [SetShouldShowNotificationUseCase]
* to check or update the visibility state of notifications.
*/
enum class NotificationId(val key: String) {
SendViaSwapTokenSelectorNotification("SendViaSwapTokenSelectorNotificationKey"),
}

View file

@ -11,7 +11,7 @@ import com.tangem.core.ui.format.bigdecimal.uncapped
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.transaction.error.GetFeeError

View file

@ -13,9 +13,9 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.StatusSource
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isZero

View file

@ -52,6 +52,7 @@ dependencies {
/** Coroutines */
implementation(deps.kotlin.coroutines)
implementation(deps.kotlin.coroutines.rx2)
implementation(deps.kotlin.datetime)
/** Logging */
implementation(deps.timber)

View file

@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.stakekit.models.request.ConstructTransactionRequestBody.GasArgs
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.domain.models.staking.action.StakingActionType
@JsonClass(generateAdapter = true)
data class PendingActionRequestBody(

View file

@ -0,0 +1,34 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.swap.DefaultSwapBestRateAnimationStore
import com.tangem.datasource.local.swap.DefaultSwapTransactionStatusStore
import com.tangem.datasource.local.swap.SwapBestRateAnimationStore
import com.tangem.datasource.local.swap.SwapTransactionStatusStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object SwapStoreModule {
@Provides
@Singleton
fun provideSwapTransactionStatusStore(): SwapTransactionStatusStore {
return DefaultSwapTransactionStatusStore(
dataStore = RuntimeDataStore(),
)
}
@Provides
@Singleton
fun provideSwapBestRateAnimationStore(): SwapBestRateAnimationStore {
return DefaultSwapBestRateAnimationStore(
dataStore = RuntimeSharedStore(),
)
}
}

View file

@ -1,23 +0,0 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.swaptx.DefaultSwapTransactionStatusStore
import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object SwapTransactionStatusStoreModule {
@Provides
@Singleton
fun provideSwapTransactionStatusStore(): SwapTransactionStatusStore {
return DefaultSwapTransactionStatusStore(
dataStore = RuntimeDataStore(),
)
}
}

View file

@ -149,6 +149,8 @@ object PreferencesKeys {
val TRON_NETWORK_FEE_NOTIFICATION_SHOW_COUNT_KEY by lazy {
intPreferencesKey(name = "tronNetworkFeeNotificationShowCount")
}
fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key")
// endregion
// region Promo

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