Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-15 11:22:40 +03:00
commit c2cd7e6b4c
675 changed files with 9577 additions and 4375 deletions

View file

@ -215,6 +215,7 @@ dependencies {
implementation(projects.data.walletManager) implementation(projects.data.walletManager)
implementation(projects.data.yieldSupply) implementation(projects.data.yieldSupply)
implementation(projects.data.hotWallet) implementation(projects.data.hotWallet)
implementation(projects.data.news)
/** Features */ /** Features */
implementation(projects.features.referral.impl) implementation(projects.features.referral.impl)

View file

@ -7,23 +7,91 @@ import dagger.hilt.android.testing.OnComponentReadyRunner
import org.junit.rules.TestRule import org.junit.rules.TestRule
import org.junit.runner.Description import org.junit.runner.Description
import org.junit.runners.model.Statement import org.junit.runners.model.Statement
import timber.log.Timber
class ApplicationInjectionExecutionRule : TestRule { class ApplicationInjectionExecutionRule(
private val toggleStates: Map<String, Boolean>
) : TestRule {
private val tangemApplication: TangemApplication private val tangemApplication: TangemApplication
get() = ApplicationProvider.getApplicationContext() get() = ApplicationProvider.getApplicationContext()
private var originalFeatureTogglesValues: Map<String, String>? = null
override fun apply(base: Statement, description: Description): Statement { override fun apply(base: Statement, description: Description): Statement {
return object : Statement() { return object : Statement() {
override fun evaluate() { override fun evaluate() {
saveOriginalFeatureToggles()
overrideFeatureToggles()
OnComponentReadyRunner.addListener( OnComponentReadyRunner.addListener(
tangemApplication, ApplicationEntryPoint::class.java tangemApplication, ApplicationEntryPoint::class.java
) { _: ApplicationEntryPoint -> ) { _: ApplicationEntryPoint ->
tangemApplication.preInit() tangemApplication.preInit()
tangemApplication.init() tangemApplication.init()
} }
base.evaluate()
try {
base.evaluate()
} finally {
restoreOriginalFeatureToggles()
}
} }
} }
} }
@Suppress("UNCHECKED_CAST")
private fun saveOriginalFeatureToggles() {
try {
val featureTogglesClass = Class.forName("com.tangem.core.configtoggle.FeatureToggles")
val valuesField = featureTogglesClass.getDeclaredField("values")
valuesField.isAccessible = true
originalFeatureTogglesValues = valuesField.get(null) as Map<String, String>
} catch (e: Exception) {
Timber.e("Failed to save original toggles values: ${e.message}")
}
}
@Suppress("UNCHECKED_CAST")
private fun overrideFeatureToggles() {
try {
val featureTogglesClass = Class.forName("com.tangem.core.configtoggle.FeatureToggles")
val valuesField = featureTogglesClass.getDeclaredField("values")
valuesField.isAccessible = true
val originalValues = originalFeatureTogglesValues ?:
(valuesField.get(null) as Map<String, String>)
val newValues = originalValues.toMutableMap()
toggleStates.forEach { (toggle, enabled) ->
if (enabled) {
newValues[toggle] = "1.0.0"
} else {
newValues.remove(toggle)
}
}
valuesField.set(null, newValues)
Timber.i("FeatureToggles.values updated: $toggleStates")
} catch (e: Exception) {
Timber.e("FeatureToggles.values didn't change with error: ${e.message}")
}
}
private fun restoreOriginalFeatureToggles() {
try {
if (originalFeatureTogglesValues != null) {
val featureTogglesClass = Class.forName("com.tangem.core.configtoggle.FeatureToggles")
val valuesField = featureTogglesClass.getDeclaredField("values")
valuesField.isAccessible = true
valuesField.set(null, originalFeatureTogglesValues)
Timber.i("FeatureToggles.values restored")
}
} catch (e: Exception) {
Timber.e("FeatureToggles.values didn't restored with error: ${e.message}")
}
}
} }

View file

@ -16,8 +16,6 @@ import com.tangem.common.allure.FailedStepScreenshotInterceptor
import com.tangem.common.constants.TestConstants.ALLURE_LABEL_NAME import com.tangem.common.constants.TestConstants.ALLURE_LABEL_NAME
import com.tangem.common.constants.TestConstants.ALLURE_LABEL_VALUE import com.tangem.common.constants.TestConstants.ALLURE_LABEL_VALUE
import com.tangem.common.rules.ApiEnvironmentRule import com.tangem.common.rules.ApiEnvironmentRule
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.PreferencesKeys
@ -53,9 +51,6 @@ abstract class BaseTestCase : TestCase(
@Inject @Inject
lateinit var appPreferencesStore: AppPreferencesStore lateinit var appPreferencesStore: AppPreferencesStore
@Inject
lateinit var featureTogglesManager: FeatureTogglesManager
@Inject @Inject
lateinit var promoRepository: PromoRepository lateinit var promoRepository: PromoRepository
@ -75,7 +70,7 @@ abstract class BaseTestCase : TestCase(
@JvmField @JvmField
val ruleChain: TestRule = RuleChain val ruleChain: TestRule = RuleChain
.outerRule(hiltRule) .outerRule(hiltRule)
.around(ApplicationInjectionExecutionRule()) .around(applicationInjectionRule())
.around(permissionRule) .around(permissionRule)
.around(apiEnvironmentRule) .around(apiEnvironmentRule)
.around(composeTestRule) .around(composeTestRule)
@ -110,7 +105,6 @@ abstract class BaseTestCase : TestCase(
apiEnvironmentRule.setup(apiConfigsManager) apiEnvironmentRule.setup(apiConfigsManager)
ActivityScenario.launch(MainActivity::class.java) ActivityScenario.launch(MainActivity::class.java)
Intents.init() Intents.init()
setFeatureToggles()
additionalBeforeSection() additionalBeforeSection()
}.after { }.after {
additionalAfterSection() additionalAfterSection()
@ -141,14 +135,15 @@ abstract class BaseTestCase : TestCase(
fun waitForIdle() = composeTestRule.waitForIdle() fun waitForIdle() = composeTestRule.waitForIdle()
private fun setFeatureToggles() { private fun applicationInjectionRule(): ApplicationInjectionExecutionRule {
runBlocking { return ApplicationInjectionExecutionRule(
with(featureTogglesManager as MutableFeatureTogglesManager) { toggleStates = mapOf(
changeToggle("NEW_TOKEN_RECEIVE_ENABLED", true) "NEW_TOKEN_RECEIVE_ENABLED" to true,
changeToggle("WALLET_BALANCE_FETCHER_ENABLED", true) "WALLET_BALANCE_FETCHER_ENABLED" to true,
changeToggle("SWAP_REDESIGN_ENABLED", true) "SWAP_REDESIGN_ENABLED" to true,
changeToggle("NEW_ONRAMP_MAIN_ENABLED", true) "NEW_ONRAMP_MAIN_ENABLED" to true,
} "HOT_WALLET_ENABLED" to true
} )
)
} }
} }

View file

@ -26,8 +26,11 @@ fun BaseTestCase.scanCard(
step("Click on 'Accept' button") { step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() } onDisclaimerScreen { acceptButton.clickWithAssertion() }
} }
step("Click on 'Scan' button") { step("Click on 'Get started' button") {
onStoriesScreen { scanButton.clickWithAssertion() } onStoriesScreen { getStartedButton.clickWithAssertion() }
}
step("Click on 'Scan card or ring' button") {
onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() }
} }
if (alreadyActivatedDialogIsShown) { if (alreadyActivatedDialogIsShown) {
step("Click on 'This is my wallet' button") { step("Click on 'This is my wallet' button") {

View file

@ -0,0 +1,23 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.BaseButtonTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.features.onboarding.v2.impl.R as OnboardingImplR
class CreateWalletStartPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<CreateWalletStartPageObject>(semanticsProvider = semanticsProvider) {
val scanCardOrRingButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(OnboardingImplR.string.welcome_unlock_card))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onCreateWalletStartScreen(function: CreateWalletStartPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -42,6 +42,7 @@ class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasTestTag(DetailsScreenTestTags.SCREEN_ITEM) hasTestTag(DetailsScreenTestTags.SCREEN_ITEM)
hasText(getResourceString(R.string.app_settings_title)) hasText(getResourceString(R.string.app_settings_title))
} }
val contactSupportButton: KNode = child { val contactSupportButton: KNode = child {
hasTestTag(DetailsScreenTestTags.SCREEN_ITEM) hasTestTag(DetailsScreenTestTags.SCREEN_ITEM)
hasText(getResourceString(R.string.common_contact_support)) hasText(getResourceString(R.string.common_contact_support))
@ -50,6 +51,11 @@ class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasTestTag(DetailsScreenTestTags.SCREEN_ITEM) hasTestTag(DetailsScreenTestTags.SCREEN_ITEM)
hasText(getResourceString(R.string.disclaimer_title)) hasText(getResourceString(R.string.disclaimer_title))
} }
val versionName: KNode = child {
hasTestTag(DetailsScreenTestTags.VERSION_NAME)
useUnmergedTree = true
}
} }
internal fun BaseTestCase.onDetailsScreen(function: DetailsPageObject.() -> Unit) = internal fun BaseTestCase.onDetailsScreen(function: DetailsPageObject.() -> Unit) =

View file

@ -10,6 +10,7 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose
import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.features.send.v2.impl.R as SendR import com.tangem.features.send.v2.impl.R as SendR
import androidx.compose.ui.test.hasText as withText
class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendPageObject>(semanticsProvider = semanticsProvider) { ComposeScreen<SendPageObject>(semanticsProvider = semanticsProvider) {
@ -38,6 +39,11 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
useUnmergedTree = true useUnmergedTree = true
} }
val amountErrorText: KNode = child {
hasTestTag(SendScreenTestTags.AMOUNT_ERROR_TEXT)
useUnmergedTree = true
}
val equivalentInputAmount: KNode = child { val equivalentInputAmount: KNode = child {
hasTestTag(SendScreenTestTags.EQUIVALENT_INPUT_AMOUNT) hasTestTag(SendScreenTestTags.EQUIVALENT_INPUT_AMOUNT)
useUnmergedTree = true useUnmergedTree = true
@ -69,8 +75,8 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
} }
val nextButton: KNode = child { val nextButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT) hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(SendR.string.common_next)) hasAnyDescendant(withText(getResourceString(SendR.string.common_next)))
useUnmergedTree = true useUnmergedTree = true
} }

View file

@ -2,20 +2,20 @@ package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.StoriesScreenTestTags import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.features.onboarding.v2.impl.R
import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class StoriesPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : class StoriesPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<StoriesPageObject>(semanticsProvider = semanticsProvider) { ComposeScreen<StoriesPageObject>(semanticsProvider = semanticsProvider) {
val scanButton: KNode = child { val getStartedButton: KNode = child {
hasTestTag(StoriesScreenTestTags.SCAN_BUTTON) hasTestTag(BaseButtonTestTags.TEXT)
} hasText(getResourceString(R.string.common_get_started))
useUnmergedTree = true
val orderButton: KNode = child {
hasTestTag(StoriesScreenTestTags.ORDER_BUTTON)
} }
} }

View file

@ -4,10 +4,7 @@ import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.openMainScreen import com.tangem.scenarios.openMainScreen
import com.tangem.screens.onDetailsScreen import com.tangem.screens.*
import com.tangem.screens.onReferralProgramScreen
import com.tangem.screens.onTopBar
import com.tangem.screens.onWalletSettingsScreen
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName import io.qameta.allure.kotlin.junit4.DisplayName
@ -31,9 +28,6 @@ class DetailsTest : BaseTestCase() {
step("Assert 'Wallet connect' button is visible") { step("Assert 'Wallet connect' button is visible") {
walletConnectButton.assertIsDisplayed() walletConnectButton.assertIsDisplayed()
} }
step("Assert 'Scan card' button is visible") {
scanCardButton.assertIsDisplayed()
}
step("Assert 'Buy Tangem card' button is visible") { step("Assert 'Buy Tangem card' button is visible") {
buyTangemButton.assertIsDisplayed() buyTangemButton.assertIsDisplayed()
} }
@ -131,9 +125,6 @@ class DetailsTest : BaseTestCase() {
step("Assert 'Wallet connect' button does not exist") { step("Assert 'Wallet connect' button does not exist") {
walletConnectButton.assertIsNotDisplayed() walletConnectButton.assertIsNotDisplayed()
} }
step("Assert 'Scan card' button is visible") {
scanCardButton.assertIsDisplayed()
}
step("Assert 'Buy Tangem card' button is visible") { step("Assert 'Buy Tangem card' button is visible") {
buyTangemButton.assertIsDisplayed() buyTangemButton.assertIsDisplayed()
} }

View file

@ -139,8 +139,11 @@ class FeedbackTest : BaseTestCase() {
step("Set scanning error") { step("Set scanning error") {
MockProvider.setEmulateError(TangemSdkError.TagLost()) MockProvider.setEmulateError(TangemSdkError.TagLost())
} }
step("Click on 'Scan' button") { step("Click on 'Get started' button") {
onStoriesScreen { scanButton.performClick() } onStoriesScreen { getStartedButton.clickWithAssertion() }
}
step("Click on 'Scan card or ring' button") {
onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() }
} }
step("Force show 'Scan warning' dialog"){ step("Force show 'Scan warning' dialog"){
runOnUiThread { runOnUiThread {
@ -178,8 +181,11 @@ class FeedbackTest : BaseTestCase() {
step("Click on 'Accept' button") { step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() } onDisclaimerScreen { acceptButton.clickWithAssertion() }
} }
step("Click on 'Scan' button") { step("Click on 'Get started' button") {
onStoriesScreen { scanButton.clickWithAssertion() } onStoriesScreen { getStartedButton.clickWithAssertion() }
}
step("Click on 'Scan card or ring' button") {
onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() }
} }
step("Check 'Already used Wallet' dialog") { step("Check 'Already used Wallet' dialog") {
checkAlreadyUsedWalletDialog() checkAlreadyUsedWalletDialog()

View file

@ -1,39 +0,0 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.onDisclaimerScreen
import com.tangem.screens.onMainScreen
import com.tangem.screens.onStoriesScreen
import com.tangem.tap.domain.sdk.mocks.MockProvider
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Test
@HiltAndroidTest
class ScanErrorTest : BaseTestCase() {
@Test
fun goToMainTest() =
setupHooks().run {
onDisclaimerScreen {
step("Click on 'Accept' button") {
acceptButton.clickWithAssertion()
}
}
onStoriesScreen {
step("Click on 'Scan' button emulating scan error") {
MockProvider.setEmulateError()
scanButton.clickWithAssertion()
}
step("Click on 'Scan' button again without emulating error") {
MockProvider.resetEmulateError()
scanButton.clickWithAssertion()
}
}
onMainScreen {
step("Make sure wallet screen is visible") {
assertIsDisplayed()
}
}
}
}

View file

@ -1,37 +0,0 @@
package com.tangem.tests
import android.content.Intent.ACTION_VIEW
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.onDisclaimerScreen
import com.tangem.screens.onStoriesScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.intent.KIntent
@HiltAndroidTest
class StoriesTest : BaseTestCase() {
// @Test
fun clickOnOrderButtonTest() =
setupHooks().run {
val buyWalletUrl = "https://buy.tangem.com/"
onDisclaimerScreen {
step("Click on 'Accept' button") {
acceptButton.clickWithAssertion()
}
}
onStoriesScreen {
step("Click on 'Order' button") {
orderButton.clickWithAssertion()
}
step("Assert: browser opened") {
val expectedIntent = KIntent {
hasAction(ACTION_VIEW)
hasData { toString().startsWith(buyWalletUrl) }
}
expectedIntent.intended()
device.uiDevice.pressBack()
}
}
}
}

View file

@ -37,8 +37,7 @@ class TermsOfServiceTest : BaseTestCase() {
} }
step("Assert 'Stories' screen is opened") { step("Assert 'Stories' screen is opened") {
onStoriesScreen { onStoriesScreen {
scanButton.assertIsDisplayed() getStartedButton.assertIsDisplayed()
orderButton.assertIsDisplayed()
} }
} }
} }
@ -91,8 +90,7 @@ class TermsOfServiceTest : BaseTestCase() {
} }
step("Assert 'Stories' screen is opened") { step("Assert 'Stories' screen is opened") {
onStoriesScreen { onStoriesScreen {
scanButton.assertIsDisplayed() getStartedButton.assertIsDisplayed()
orderButton.assertIsDisplayed()
} }
} }
step("Stop app") { step("Stop app") {
@ -103,8 +101,7 @@ class TermsOfServiceTest : BaseTestCase() {
} }
step("Assert 'Stories' screen is opened") { step("Assert 'Stories' screen is opened") {
onStoriesScreen { onStoriesScreen {
scanButton.assertIsDisplayed() getStartedButton.assertIsDisplayed()
orderButton.assertIsDisplayed()
} }
} }
} }

View file

@ -276,7 +276,10 @@ class RecentBlockTest : BaseTestCase() {
} }
step("Swipe up") { step("Swipe up") {
waitForIdle() waitForIdle()
swipeVertical(SwipeDirection.UP, startHeightRatio = 0.6f) onSendAddressScreen {
swipeVertical(SwipeDirection.UP, startHeightRatio = 0.6f)
swipeVertical(SwipeDirection.UP, startHeightRatio = 0.6f)
}
} }
step("Check recent address item №7") { step("Check recent address item №7") {
checkRecentAddressItem(address = recipientAddressBase + "f", description = recentTransactionAmount2) checkRecentAddressItem(address = recipientAddressBase + "f", description = recentTransactionAmount2)

View file

@ -0,0 +1,145 @@
package com.tangem.tests.send.amountScreen
import android.view.KeyEvent
import com.tangem.common.BaseTestCase
import com.tangem.common.utils.setClipboardText
import com.tangem.scenarios.*
import com.tangem.screens.*
import com.tangem.wallet.R
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class SendAmountScreenTest : BaseTestCase() {
@AllureId("4761")
@DisplayName("Send (amount screen): validate different amounts")
@Test
fun validateDifferentAmountsTest() {
val tokenName = "Ethereum"
val manualSendAmount = "1"
val clipboardSendAmount = "0.5"
val invalidAmount = "2"
val errorText = getResourceString(R.string.send_validation_amount_exceeds_balance)
val context = device.context
setupHooks().run {
step("Open 'Send' screen") {
openSendScreen(tokenName)
}
step("Type '$manualSendAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(manualSendAmount)
}
}
step("Assert send amount = '$manualSendAmount'") {
onSendScreen { amountInputTextField.assertTextContains(manualSendAmount, substring = true) }
}
step("Assert 'Next' button is enabled") {
onSendScreen { nextButton.assertIsEnabled() }
}
step("Press system 'Delete' button") {
waitForIdle()
device.uiDevice.pressDelete()
}
step("Set clipboard text") {
setClipboardText(context, clipboardSendAmount)
}
step("Click on input text field") {
onSendScreen { amountInputTextField.performClick() }
}
step("Paste from clipboard") {
device.uiDevice.pressKeyCode(KeyEvent.KEYCODE_V, KeyEvent.META_CTRL_ON)
}
step("Assert send amount = '$clipboardSendAmount'") {
onSendScreen { amountInputTextField.assertTextContains(clipboardSendAmount, substring = true) }
}
step("Assert 'Next' button is enabled") {
onSendScreen { nextButton.assertIsEnabled() }
}
step("Press system 'Delete' button") {
waitForIdle()
device.uiDevice.pressDelete()
}
step("Type '$invalidAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(invalidAmount)
}
}
step("Assert send amount = '$invalidAmount'") {
onSendScreen { amountInputTextField.assertTextContains(invalidAmount, substring = true) }
}
step("Assert amount error contains text '$errorText'") {
onSendScreen { amountErrorText.assertTextContains(errorText) }
}
step("Assert 'Next' button is disabled") {
onSendScreen { nextButton.assertIsNotEnabled() }
}
step("Click on 'Max' button") {
onSendScreen { maxButton.performClick() }
}
step("Assert send amount = '$manualSendAmount'") {
onSendScreen { amountInputTextField.assertTextContains(manualSendAmount, substring = true) }
}
step("Assert 'Next' button is enabled") {
onSendScreen { nextButton.assertIsEnabled() }
}
step("Click on 'Close' button") {
onSendScreen { closeButton.performClick() }
}
step("Assert 'Token Details' screen is displayed") {
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
}
}
}
@AllureId("4762")
@DisplayName("Send (amount screen): switch equivalent")
@Test
fun switchEquivalentTest() {
val tokenName = "Ethereum"
val tokenAmount = "1"
val fiatAmount = "$2,535.63"
setupHooks().run {
step("Open 'Send' screen") {
openSendScreen(tokenName)
}
step("Type '$tokenAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(tokenAmount)
}
}
step("Assert send amount = '$tokenAmount'") {
onSendScreen { amountInputTextField.assertTextContains(tokenAmount, substring = true) }
}
step("Assert equivalent amount = '$fiatAmount'") {
onSendScreen { equivalentInputAmount.assertTextContains(fiatAmount, substring = true) }
}
step("Click on 'Exchange' button") {
onSendScreen { exchangeIcon.performClick() }
}
step("Assert send amount = '$fiatAmount'") {
onSendScreen { amountInputTextField.assertTextContains(fiatAmount, substring = true) }
}
step("Assert equivalent amount = '$tokenAmount'") {
onSendScreen { equivalentInputAmount.assertTextContains(tokenAmount, substring = true) }
}
step("Click on 'Exchange' button") {
onSendScreen { exchangeIcon.performClick() }
}
step("Assert send amount = '$tokenAmount'") {
onSendScreen { amountInputTextField.assertTextContains(tokenAmount, substring = true) }
}
step("Assert equivalent amount = '$fiatAmount'") {
onSendScreen { equivalentInputAmount.assertTextContains(fiatAmount, substring = true) }
}
}
}
}

View file

@ -22,7 +22,7 @@ class SolanaWarningsTest : BaseTestCase() {
private val tokenName = "Solana" private val tokenName = "Solana"
private val amountToLeaveLessThanRent = "0.0016941" private val amountToLeaveLessThanRent = "0.0016941"
private val amountToLeaveGreaterThanRent = "0.0000941" private val amountToLeaveGreaterThanRent = "0.0000941"
private val amountToLeaveRentOnly = "0.00168934" private val amountToLeaveRentOnly = "0.001689338"
private val rentAmount = "SOL 0.00089088" private val rentAmount = "SOL 0.00089088"
private val invalidAmountTitle = getResourceString(R.string.send_notification_invalid_amount_title) private val invalidAmountTitle = getResourceString(R.string.send_notification_invalid_amount_title)

View file

@ -19,7 +19,7 @@
<uses-feature android:name="android.hardware.camera.autofocus" /> <uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature <uses-feature
android:name="android.hardware.nfc" android:name="android.hardware.nfc"
android:required="true" /> android:required="false" />
<queries> <queries>
<intent> <intent>

View file

@ -367,7 +367,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
val isFromPush = intent.extras?.containsKey(OPENED_FROM_GCM_PUSH) == true val isFromPush = intent.extras?.containsKey(OPENED_FROM_GCM_PUSH) == true
if (isFromPush) { if (isFromPush) {
analyticsEventsHandler.send(Push.PushNotificationOpened) analyticsEventsHandler.send(Push.PushNotificationOpened())
} }
handleDeepLink(intent = intent, isFromOnNewIntent = true) handleDeepLink(intent = intent, isFromOnNewIntent = true)

View file

@ -328,7 +328,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
ExceptionHandler.append(blockchainExceptionHandler) ExceptionHandler.append(blockchainExceptionHandler)
if (LogConfig.network.blockchainSdkNetwork) { if (LogConfig.network.isBlockchainSdkNetworkLogEnabled) {
BlockchainSdkRetrofitBuilder.interceptors = listOf( BlockchainSdkRetrofitBuilder.interceptors = listOf(
createNetworkLoggingInterceptor(), createNetworkLoggingInterceptor(),
ChuckerInterceptor(this), ChuckerInterceptor(this),

View file

@ -25,8 +25,6 @@ internal class DefaultTrackingContextProxy(private val abTestsManager: ABTestsMa
override fun setContext(scanResponse: ScanResponse) { override fun setContext(scanResponse: ScanResponse) {
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
Analytics.setContext(userWalletId, scanResponse)
abTestsManager.setUserProperties( abTestsManager.setUserProperties(
userId = calculateUserIdHash(userWalletId), userId = calculateUserIdHash(userWalletId),
batch = scanResponse.card.batchId, batch = scanResponse.card.batchId,

View file

@ -12,12 +12,6 @@ sealed class AnalyticsParam {
class Amount(amount: com.tangem.blockchain.common.Amount) : CurrencyType(amount.currencySymbol) class Amount(amount: com.tangem.blockchain.common.Amount) : CurrencyType(amount.currencySymbol)
} }
sealed class CardBalanceState(val value: String) {
data object Empty : CardBalanceState("Empty")
data object Full : CardBalanceState("Full")
companion object
}
sealed class RateApp(val value: String) { sealed class RateApp(val value: String) {
data object Liked : RateApp("Liked") data object Liked : RateApp("Liked")
data object Closed : RateApp("Close") data object Closed : RateApp("Close")

View file

@ -11,85 +11,5 @@ sealed class Onboarding(
params: Map<String, String> = emptyMap(), params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category, event, params) { ) : AnalyticsEvent(category, event, params) {
class Started : Onboarding("Onboarding", "Onboarding Started")
class Finished : Onboarding("Onboarding", "Onboarding Finished") class Finished : Onboarding("Onboarding", "Onboarding Finished")
sealed class CreateWallet(
event: String,
params: Map<String, String> = emptyMap(),
) : Onboarding("Onboarding / Create Wallet", event, params) {
class ScreenOpened : CreateWallet("Create Wallet Screen Opened")
class ButtonCreateWallet : CreateWallet("Button - Create Wallet")
class WalletCreatedSuccessfully(
creationType: AnalyticsParam.WalletCreationType = AnalyticsParam.WalletCreationType.PrivateKey,
seedPhraseLength: Int? = null,
) : CreateWallet(
event = "Wallet Created Successfully",
params = buildMap {
put(AnalyticsParam.CREATION_TYPE, creationType.value)
if (seedPhraseLength != null) put(AnalyticsParam.SEED_PHRASE_LENGTH, seedPhraseLength.toString())
},
)
}
sealed class Backup(
event: String,
params: Map<String, String> = emptyMap(),
) : Onboarding("Onboarding / Backup", event, params) {
class ScreenOpened : Backup("Backup Screen Opened")
class Started : Backup("Backup Started")
class Skipped : Backup("Backup Skipped")
class SettingAccessCodeStarted : Backup("Setting Access Code Started")
class AccessCodeEntered : Backup("Access Code Entered")
class AccessCodeReEntered : Backup("Access Code Re-entered")
class Finished(cardsCount: Int) : Backup(
event = "Backup Finished",
params = mapOf("Cards count" to "$cardsCount"),
)
object ResetCancelEvent : Backup(
event = "Reset Card Notification",
params = mapOf("Option" to "Cancel"),
)
object ResetPerformEvent : Backup(
event = "Reset Card Notification",
params = mapOf("Option" to "Reset"),
)
}
sealed class Topup(
event: String,
params: Map<String, String> = emptyMap(),
) : Onboarding("Onboarding / Top Up", event, params) {
class ScreenOpened : Topup("Activation Screen Opened")
class ButtonBuyCrypto(currency: AnalyticsParam.CurrencyType) : Topup(
event = "Button - Buy Crypto",
params = mapOf(AnalyticsParam.CURRENCY to currency.value),
)
class ButtonShowWalletAddress : Topup("Button - Show the Wallet Address")
}
sealed class Twins(
event: String,
params: Map<String, String> = emptyMap(),
) : Onboarding("Onboarding / Twins", event, params) {
class ScreenOpened : Twins("Twinning Screen Opened")
class SetupStarted : Twins("Twin Setup Started")
class SetupFinished : Twins("Twin Setup Finished")
}
class EnableBiometrics(state: AnalyticsParam.OnOffState) : Onboarding(
category = "Onboarding / Biometric",
event = "Enable Biometric",
params = mapOf("State" to state.value),
)
} }

View file

@ -8,5 +8,5 @@ internal sealed class Push(event: String) : AnalyticsEvent(
params = emptyMap(), params = emptyMap(),
) { ) {
data object PushNotificationOpened : Push(event = "Push Notification Opened") class PushNotificationOpened : Push(event = "Push Notification Opened")
} }

View file

@ -67,7 +67,7 @@ sealed class Settings(
params = mapOf("State" to state.value), params = mapOf("State" to state.value),
) )
object ButtonEnableBiometricAuthentication : AppSettings(event = "Button - Enable Biometric Authentication") class ButtonEnableBiometricAuthentication : AppSettings(event = "Button - Enable Biometric Authentication")
class MainCurrencyChanged(currencyType: String) : AppSettings( class MainCurrencyChanged(currencyType: String) : AppSettings(
event = "Main Currency Changed", event = "Main Currency Changed",
@ -79,7 +79,7 @@ sealed class Settings(
params = mapOf("State" to theme.value), params = mapOf("State" to theme.value),
) )
object EnableBiometrics : AppSettings(event = "Notice - Enable Biometric") class EnableBiometrics : AppSettings(event = "Notice - Enable Biometric")
class HideBalanceChanged(state: AnalyticsParam.OnOffState) : AppSettings( class HideBalanceChanged(state: AnalyticsParam.OnOffState) : AppSettings(
event = "Hide Balance Changed", event = "Hide Balance Changed",

View file

@ -10,8 +10,6 @@ sealed class SignIn(
params: Map<String, String> = emptyMap(), params: Map<String, String> = emptyMap(),
) : AnalyticsEvent("Sign In", event, params) { ) : AnalyticsEvent("Sign In", event, params) {
class ScreenOpened : SignIn(event = "Sign In Screen Opened")
class ButtonBiometricSignIn : SignIn(event = "Button - Biometric Sign In") class ButtonBiometricSignIn : SignIn(event = "Button - Biometric Sign In")
class ButtonCardSignIn : SignIn(event = "Button - Card Sign In") class ButtonCardSignIn : SignIn(event = "Button - Card Sign In")
} }

View file

@ -29,7 +29,7 @@ class AmplitudeAnalyticsHandler(
class Builder : AnalyticsHandlerBuilder { class Builder : AnalyticsHandlerBuilder {
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler { override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler {
return AmplitudeAnalyticsHandler( return AmplitudeAnalyticsHandler(
client = if (data.logConfig.amplitude) { client = if (data.logConfig.isAmplitudeLogEnabled) {
AmplitudeLogClient(data.jsonConverter) AmplitudeLogClient(data.jsonConverter)
} else { } else {
AmplitudeClient(data.application, data.config.amplitudeApiKey) AmplitudeClient(data.application, data.config.amplitudeApiKey)

View file

@ -49,7 +49,7 @@ class FirebaseAnalyticsHandler(
class Builder : AnalyticsHandlerBuilder { class Builder : AnalyticsHandlerBuilder {
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = when { override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = when {
!data.isDebug -> FirebaseClient() !data.isDebug -> FirebaseClient()
data.isDebug && data.logConfig.firebase -> FirebaseLogClient(data.jsonConverter) data.isDebug && data.logConfig.isFirebaseLogEnabled -> FirebaseLogClient(data.jsonConverter)
else -> null else -> null
}?.let { FirebaseAnalyticsHandler(it) } }?.let { FirebaseAnalyticsHandler(it) }
} }

View file

@ -3,6 +3,7 @@ package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.core.analytics.api.ParamsInterceptor import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.SignIn
import com.tangem.domain.card.analytics.IntroductionProcess import com.tangem.domain.card.analytics.IntroductionProcess
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver
@ -30,7 +31,11 @@ class CardContextInterceptor(
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean { override fun canBeAppliedTo(event: AnalyticsEvent): Boolean {
return when (event) { return when (event) {
is IntroductionProcess.ButtonScanCard -> false is IntroductionProcess.ButtonScanCard,
is IntroductionProcess.ButtonScanCardLegacy,
is SignIn.ScreenOpened,
is SignIn.ButtonAddWallet,
-> false
else -> true else -> true
} }
} }

View file

@ -3,6 +3,8 @@ package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.core.analytics.api.ParamsInterceptor import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.SignIn
import com.tangem.domain.card.analytics.IntroductionProcess
class HotWalletContextInterceptor( class HotWalletContextInterceptor(
val parent: ParamsInterceptor? = null, val parent: ParamsInterceptor? = null,
@ -10,10 +12,22 @@ class HotWalletContextInterceptor(
override fun id(): String = HotWalletContextInterceptor.id() override fun id(): String = HotWalletContextInterceptor.id()
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = true override fun canBeAppliedTo(event: AnalyticsEvent): Boolean {
return when (event) {
is SignIn.ScreenOpened,
is SignIn.ButtonAddWallet,
is SignIn.ButtonUnlockAllWithBiometric,
is IntroductionProcess.ButtonScanCard,
-> false
else -> true
}
}
override fun intercept(params: MutableMap<String, String>) { override fun intercept(params: MutableMap<String, String>) {
params[AnalyticsParam.PRODUCT_TYPE] = AnalyticsParam.ProductType.MobileWallet.value params[AnalyticsParam.PRODUCT_TYPE] = AnalyticsParam.ProductType.MobileWallet.value
params.remove(AnalyticsParam.BATCH)
params.remove(AnalyticsParam.FIRMWARE)
params.remove(AnalyticsParam.CURRENCY)
} }
companion object { companion object {

View file

@ -67,11 +67,13 @@ internal object AccountDomainModule {
accountsCRUDRepository: AccountsCRUDRepository, accountsCRUDRepository: AccountsCRUDRepository,
mainAccountTokensMigration: MainAccountTokensMigration, mainAccountTokensMigration: MainAccountTokensMigration,
cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher, cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher,
singleAccountListFetcher: SingleAccountListFetcher,
): RecoverCryptoPortfolioUseCase { ): RecoverCryptoPortfolioUseCase {
return RecoverCryptoPortfolioUseCase( return RecoverCryptoPortfolioUseCase(
crudRepository = accountsCRUDRepository, crudRepository = accountsCRUDRepository,
mainAccountTokensMigration = mainAccountTokensMigration, mainAccountTokensMigration = mainAccountTokensMigration,
cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher, cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher,
singleAccountListFetcher = singleAccountListFetcher,
) )
} }

View file

@ -1,6 +1,8 @@
package com.tangem.tap.di.domain package com.tangem.tap.di.domain
import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase
import com.tangem.domain.hotwallet.IsHotWalletCreationSupported
import com.tangem.domain.hotwallet.IsAccessCodeSimpleUseCase
import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase
import com.tangem.domain.hotwallet.repository.HotWalletRepository import com.tangem.domain.hotwallet.repository.HotWalletRepository
import dagger.Module import dagger.Module
@ -24,4 +26,18 @@ internal object HotWalletDomainModule {
fun provideSetAccessCodeSkippedUseCase(hotWalletRepository: HotWalletRepository): SetAccessCodeSkippedUseCase { fun provideSetAccessCodeSkippedUseCase(hotWalletRepository: HotWalletRepository): SetAccessCodeSkippedUseCase {
return SetAccessCodeSkippedUseCase(hotWalletRepository) return SetAccessCodeSkippedUseCase(hotWalletRepository)
} }
@Provides
@Singleton
fun provideIsAccessCodeSimpleUseCase(): IsAccessCodeSimpleUseCase {
return IsAccessCodeSimpleUseCase()
}
@Provides
@Singleton
fun provideIsWalletCreationSupportedUseCase(
hotWalletRepository: HotWalletRepository,
): IsHotWalletCreationSupported {
return IsHotWalletCreationSupported(hotWalletRepository)
}
} }

View file

@ -6,7 +6,7 @@ import com.tangem.domain.managetokens.repository.ManageTokensRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.wallets.derivations.DerivationsRepository
@ -74,7 +74,7 @@ internal object ManageTokensDomainModule {
derivationsRepository: DerivationsRepository, derivationsRepository: DerivationsRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
stakingIdFactory: StakingIdFactory, stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
): SaveManagedTokensUseCase { ): SaveManagedTokensUseCase {
@ -85,7 +85,7 @@ internal object ManageTokensDomainModule {
derivationsRepository = derivationsRepository, derivationsRepository = derivationsRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiStakingBalanceFetcher = multiStakingBalanceFetcher,
stakingIdFactory = stakingIdFactory, stakingIdFactory = stakingIdFactory,
parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default), parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default),
) )

View file

@ -10,8 +10,9 @@ import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.hotwallet.HotWalletFeatureToggles
@ -65,20 +66,22 @@ object MarketsDomainModule {
fun provideSaveMarketTokensUseCase( fun provideSaveMarketTokensUseCase(
derivationsRepository: DerivationsRepository, derivationsRepository: DerivationsRepository,
marketsTokenRepository: MarketsTokenRepository, marketsTokenRepository: MarketsTokenRepository,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
stakingIdFactory: StakingIdFactory, stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
): SaveMarketTokensUseCase { ): SaveMarketTokensUseCase {
return SaveMarketTokensUseCase( return SaveMarketTokensUseCase(
derivationsRepository = derivationsRepository, derivationsRepository = derivationsRepository,
marketsTokenRepository = marketsTokenRepository, marketsTokenRepository = marketsTokenRepository,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiStakingBalanceFetcher = multiStakingBalanceFetcher,
stakingIdFactory = stakingIdFactory, stakingIdFactory = stakingIdFactory,
parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default), parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default),
) )

View file

@ -1,11 +1,11 @@
package com.tangem.tap.di.domain package com.tangem.tap.di.domain
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.nft.* import com.tangem.domain.nft.*
import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.nft.utils.NFTCleaner
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
@ -27,12 +27,12 @@ internal object NFTDomainModule {
fun providesGetNFTCollectionsUseCase( fun providesGetNFTCollectionsUseCase(
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
nftRepository: NFTRepository, nftRepository: NFTRepository,
singleAccountStatusListSupplier: SingleAccountStatusListSupplier, singleAccountListSupplier: SingleAccountListSupplier,
accountsFeatureToggles: AccountsFeatureToggles, accountsFeatureToggles: AccountsFeatureToggles,
): GetNFTCollectionsUseCase = GetNFTCollectionsUseCase( ): GetNFTCollectionsUseCase = GetNFTCollectionsUseCase(
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
nftRepository = nftRepository, nftRepository = nftRepository,
singleAccountStatusListSupplier = singleAccountStatusListSupplier, singleAccountListSupplier = singleAccountListSupplier,
accountsFeatureToggles = accountsFeatureToggles, accountsFeatureToggles = accountsFeatureToggles,
) )
@ -67,12 +67,12 @@ internal object NFTDomainModule {
@Singleton @Singleton
fun providesGetNFTAvailableNetworksUseCase( fun providesGetNFTAvailableNetworksUseCase(
nftRepository: NFTRepository, nftRepository: NFTRepository,
singleAccountStatusListSupplier: SingleAccountStatusListSupplier, singleAccountListSupplier: SingleAccountListSupplier,
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
): GetNFTNetworksUseCase = GetNFTNetworksUseCase( ): GetNFTNetworksUseCase = GetNFTNetworksUseCase(
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
nftRepository = nftRepository, nftRepository = nftRepository,
singleAccountStatusListSupplier = singleAccountStatusListSupplier, singleAccountListSupplier = singleAccountListSupplier,
) )
@Provides @Provides
@ -126,13 +126,13 @@ internal object NFTDomainModule {
@Singleton @Singleton
fun provideDisableWalletNFTUseCase( fun provideDisableWalletNFTUseCase(
walletsRepository: WalletsRepository, walletsRepository: WalletsRepository,
nftRepository: NFTRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
nftCleaner: NFTCleaner,
): DisableWalletNFTUseCase { ): DisableWalletNFTUseCase {
return DisableWalletNFTUseCase( return DisableWalletNFTUseCase(
walletsRepository = walletsRepository, walletsRepository = walletsRepository,
nftRepository = nftRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
nftCleaner = nftCleaner,
) )
} }
@ -145,13 +145,13 @@ internal object NFTDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideClearNFTCacheUseCase( fun provideClearNFTCacheUseCase(
nftRepository: NFTRepository, nftCleaner: NFTCleaner,
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
accountsFeatureToggles: AccountsFeatureToggles, accountsFeatureToggles: AccountsFeatureToggles,
singleAccountListSupplier: SingleAccountListSupplier, singleAccountListSupplier: SingleAccountListSupplier,
): ObserveAndClearNFTCacheIfNeedUseCase { ): ObserveAndClearNFTCacheIfNeedUseCase {
return ObserveAndClearNFTCacheIfNeedUseCase( return ObserveAndClearNFTCacheIfNeedUseCase(
nftRepository = nftRepository, nftCleaner = nftCleaner,
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
accountsFeatureToggles = accountsFeatureToggles, accountsFeatureToggles = accountsFeatureToggles,
singleAccountListSupplier = singleAccountListSupplier, singleAccountListSupplier = singleAccountListSupplier,

View file

@ -1,10 +1,7 @@
package com.tangem.tap.di.domain package com.tangem.tap.di.domain
import com.tangem.domain.news.repository.NewsRepository import com.tangem.domain.news.repository.NewsRepository
import com.tangem.domain.news.usecase.GetNewsCategoriesUseCase import com.tangem.domain.news.usecase.*
import com.tangem.domain.news.usecase.GetNewsListBatchFlowUseCase
import com.tangem.domain.news.usecase.ObserveNewsDetailsUseCase
import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -38,4 +35,10 @@ internal object NewsDomainModule {
fun provideGetNewsListBatchFlowUseCase(repository: NewsRepository): GetNewsListBatchFlowUseCase { fun provideGetNewsListBatchFlowUseCase(repository: NewsRepository): GetNewsListBatchFlowUseCase {
return GetNewsListBatchFlowUseCase(repository) return GetNewsListBatchFlowUseCase(repository)
} }
@Provides
@Singleton
fun provideFetchTrendingNewsUseCase(repository: NewsRepository): FetchTrendingNewsUseCase {
return FetchTrendingNewsUseCase(repository)
}
} }

View file

@ -2,6 +2,7 @@ package com.tangem.tap.di.domain
import com.tangem.domain.onramp.* import com.tangem.domain.onramp.*
import com.tangem.domain.onramp.repositories.* import com.tangem.domain.onramp.repositories.*
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -264,12 +265,14 @@ internal object OnrampDomainModule {
onrampErrorResolver: OnrampErrorResolver, onrampErrorResolver: OnrampErrorResolver,
onrampTransactionRepository: OnrampTransactionRepository, onrampTransactionRepository: OnrampTransactionRepository,
settingsRepository: SettingsRepository, settingsRepository: SettingsRepository,
promoRepository: PromoRepository,
): GetOnrampOffersUseCase { ): GetOnrampOffersUseCase {
return GetOnrampOffersUseCase( return GetOnrampOffersUseCase(
onrampRepository = onrampRepository, onrampRepository = onrampRepository,
errorResolver = onrampErrorResolver, errorResolver = onrampErrorResolver,
onrampTransactionRepository = onrampTransactionRepository, onrampTransactionRepository = onrampTransactionRepository,
settingsRepository = settingsRepository, settingsRepository = settingsRepository,
promoRepository = promoRepository,
) )
} }
} }

View file

@ -7,8 +7,8 @@ import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakeKitRepository import com.tangem.domain.staking.repositories.StakeKitRepository
import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.repositories.StakeKitTransactionHashRepository import com.tangem.domain.staking.repositories.StakeKitTransactionHashRepository
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
import com.tangem.domain.staking.toggles.StakingFeatureToggles import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import dagger.Module import dagger.Module
@ -113,11 +113,11 @@ internal object StakingDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideFetchStakingYieldBalanceUseCase( fun provideFetchStakingYieldBalanceUseCase(
singleYieldBalanceFetcher: SingleYieldBalanceFetcher, singleStakingBalanceFetcher: SingleStakingBalanceFetcher,
stakingIdFactory: StakingIdFactory, stakingIdFactory: StakingIdFactory,
): FetchStakingYieldBalanceUseCase { ): FetchStakingYieldBalanceUseCase {
return FetchStakingYieldBalanceUseCase( return FetchStakingYieldBalanceUseCase(
singleYieldBalanceFetcher = singleYieldBalanceFetcher, singleStakingBalanceFetcher = singleStakingBalanceFetcher,
stakingIdFactory = stakingIdFactory, stakingIdFactory = stakingIdFactory,
) )
} }

View file

@ -12,15 +12,18 @@ import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier
import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.staking.single.SingleStakingBalanceSupplier
import com.tangem.domain.tokens.* import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.* import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles
@ -40,17 +43,19 @@ internal object TokensDomainModule {
@Singleton @Singleton
fun provideAddCryptoCurrenciesUseCase( fun provideAddCryptoCurrenciesUseCase(
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher, singleStakingBalanceFetcher: SingleStakingBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory, stakingIdFactory: StakingIdFactory,
): AddCryptoCurrenciesUseCase { ): AddCryptoCurrenciesUseCase {
return AddCryptoCurrenciesUseCase( return AddCryptoCurrenciesUseCase(
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher, singleStakingBalanceFetcher = singleStakingBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
stakingIdFactory = stakingIdFactory, stakingIdFactory = stakingIdFactory,
) )
@ -148,7 +153,7 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher, singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher, singleStakingBalanceFetcher: SingleStakingBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory, stakingIdFactory: StakingIdFactory,
): FetchCurrencyStatusUseCase { ): FetchCurrencyStatusUseCase {
@ -156,7 +161,7 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
singleNetworkStatusFetcher = singleNetworkStatusFetcher, singleNetworkStatusFetcher = singleNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher, singleStakingBalanceFetcher = singleStakingBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
stakingIdFactory = stakingIdFactory, stakingIdFactory = stakingIdFactory,
) )
@ -339,8 +344,8 @@ internal object TokensDomainModule {
singleNetworkStatusSupplier: SingleNetworkStatusSupplier, singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
singleQuoteStatusSupplier: SingleQuoteStatusSupplier, singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier, singleStakingBalanceSupplier: SingleStakingBalanceSupplier,
multiYieldBalanceSupplier: MultiYieldBalanceSupplier, multiStakingBalanceSupplier: MultiStakingBalanceSupplier,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory, stakingIdFactory: StakingIdFactory,
): BaseCurrencyStatusOperations { ): BaseCurrencyStatusOperations {
@ -350,8 +355,8 @@ internal object TokensDomainModule {
singleNetworkStatusSupplier = singleNetworkStatusSupplier, singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier,
singleQuoteStatusSupplier = singleQuoteStatusSupplier, singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier, singleStakingBalanceSupplier = singleStakingBalanceSupplier,
multiYieldBalanceSupplier = multiYieldBalanceSupplier, multiStakingBalanceSupplier = multiStakingBalanceSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
stakingIdFactory = stakingIdFactory, stakingIdFactory = stakingIdFactory,
) )
@ -371,7 +376,7 @@ internal object TokensDomainModule {
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
stakingIdFactory: StakingIdFactory, stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
): WalletBalanceFetcher { ): WalletBalanceFetcher {
@ -381,7 +386,7 @@ internal object TokensDomainModule {
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiStakingBalanceFetcher = multiStakingBalanceFetcher,
stakingIdFactory = stakingIdFactory, stakingIdFactory = stakingIdFactory,
dispatchers = dispatchers, dispatchers = dispatchers,
) )

View file

@ -164,7 +164,7 @@ internal class LegacyScanProcessor @Inject constructor(
) { ) {
if (error is TangemSdkError.CardVerificationFailed) { if (error is TangemSdkError.CardVerificationFailed) {
analyticsEventHandler.send( analyticsEventHandler.send(
event = OnboardingAnalyticsEvent.Onboarding.OfflineAttestationFailed( event = OnboardingAnalyticsEvent.Error.OfflineAttestationFailed(
analyticsSource, analyticsSource,
), ),
) )

View file

@ -7,8 +7,10 @@ import com.tangem.common.authentication.storage.AuthenticatedStorage
import com.tangem.common.json.TangemSdkAdapter import com.tangem.common.json.TangemSdkAdapter
import com.tangem.common.services.secure.SecureStorage import com.tangem.common.services.secure.SecureStorage
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.hotwallet.repository.HotWalletRepository
import com.tangem.domain.models.scan.serialization.* import com.tangem.domain.models.scan.serialization.*
import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.model.VisaCardActivationStatus import com.tangem.domain.visa.model.VisaCardActivationStatus
@ -125,6 +127,9 @@ internal object UserWalletsListManagerModule {
appPreferencesStore: AppPreferencesStore, appPreferencesStore: AppPreferencesStore,
hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
tangemHotSdk: TangemHotSdk, tangemHotSdk: TangemHotSdk,
trackingContextProxy: TrackingContextProxy,
analyticsEventHandler: AnalyticsEventHandler,
hotWalletRepository: HotWalletRepository,
): UserWalletsListRepository { ): UserWalletsListRepository {
val moshi = buildMoshi() val moshi = buildMoshi()
val secureStorage = buildSecureStorage(applicationContext = applicationContext) val secureStorage = buildSecureStorage(applicationContext = applicationContext)
@ -172,6 +177,9 @@ internal object UserWalletsListManagerModule {
savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now
hotWalletAccessCodeAttemptsRepository = hotWalletAccessCodeAttemptsRepository, hotWalletAccessCodeAttemptsRepository = hotWalletAccessCodeAttemptsRepository,
tangemHotSdk = tangemHotSdk, tangemHotSdk = tangemHotSdk,
trackingContextProxy = trackingContextProxy,
analyticsEventHandler = analyticsEventHandler,
hotWalletRepository = hotWalletRepository,
) )
} }

View file

@ -7,12 +7,16 @@ import arrow.core.raise.either
import arrow.core.right import arrow.core.right
import com.tangem.common.* import com.tangem.common.*
import com.tangem.common.core.TangemSdkError import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository.LockMethod import com.tangem.domain.common.wallets.UserWalletsListRepository.LockMethod
import com.tangem.domain.common.wallets.error.* import com.tangem.domain.common.wallets.error.*
import com.tangem.domain.hotwallet.repository.HotWalletRepository
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isLocked
@ -50,6 +54,9 @@ internal class DefaultUserWalletsListRepository(
private val appPreferencesStore: AppPreferencesStore, private val appPreferencesStore: AppPreferencesStore,
private val hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, private val hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
private val tangemHotSdk: TangemHotSdk, private val tangemHotSdk: TangemHotSdk,
private val trackingContextProxy: TrackingContextProxy,
private val analyticsEventHandler: AnalyticsEventHandler,
private val hotWalletRepository: HotWalletRepository,
) : UserWalletsListRepository { ) : UserWalletsListRepository {
override val userWallets = MutableStateFlow<List<UserWallet>?>(null) override val userWallets = MutableStateFlow<List<UserWallet>?>(null)
@ -204,7 +211,7 @@ internal class DefaultUserWalletsListRepository(
userWalletEncryptionKeysRepository.delete(userWalletIds) userWalletEncryptionKeysRepository.delete(userWalletIds)
removeHotWalletsFromSDK(userWalletIds) removeHotWalletsFromSDKAndRepos(userWalletIds)
userWallets.update { currentWallets -> userWallets.update { currentWallets ->
val updatedWallets = currentWallets?.filter { userWalletIds.contains(it.walletId).not() } val updatedWallets = currentWallets?.filter { userWalletIds.contains(it.walletId).not() }
@ -235,6 +242,7 @@ internal class DefaultUserWalletsListRepository(
when (unlockMethod) { when (unlockMethod) {
UserWalletsListRepository.UnlockMethod.Biometric -> { UserWalletsListRepository.UnlockMethod.Biometric -> {
unlockAllWallets().bind() unlockAllWallets().bind()
trackSignInEvent(userWallet, Basic.SignedIn.SignInType.Biometric)
select(userWalletId) select(userWalletId)
} }
UserWalletsListRepository.UnlockMethod.AccessCode -> { UserWalletsListRepository.UnlockMethod.AccessCode -> {
@ -263,7 +271,10 @@ internal class DefaultUserWalletsListRepository(
removePasswordAttempts(userWallet) removePasswordAttempts(userWallet)
sensitiveInformationRepository.getAll(listOf(encryptionKey)) sensitiveInformationRepository.getAll(listOf(encryptionKey))
.doOnSuccess { sensitiveInfo -> updateWallets { it?.updateWith(sensitiveInfo) } } .doOnSuccess { sensitiveInfo ->
updateWallets { it?.updateWith(sensitiveInfo) }
trackSignInEvent(userWallet, Basic.SignedIn.SignInType.AccessCode)
}
.doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) } .doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) }
} }
is UserWalletsListRepository.UnlockMethod.Scan -> { is UserWalletsListRepository.UnlockMethod.Scan -> {
@ -291,7 +302,10 @@ internal class DefaultUserWalletsListRepository(
) )
sensitiveInformationRepository.getAll(listOf(encryptionKey)) sensitiveInformationRepository.getAll(listOf(encryptionKey))
.doOnSuccess { sensitiveInfo -> updateWallets { it?.updateWith(sensitiveInfo) } } .doOnSuccess { sensitiveInfo ->
updateWallets { it?.updateWith(sensitiveInfo) }
trackSignInEvent(userWallet, Basic.SignedIn.SignInType.Card)
}
.doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) } .doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) }
} }
} }
@ -332,6 +346,9 @@ internal class DefaultUserWalletsListRepository(
sensitiveInformationRepository.getAll(allKeys) sensitiveInformationRepository.getAll(allKeys)
.doOnSuccess { sensitiveInfo -> .doOnSuccess { sensitiveInfo ->
updateWallets { wallets -> wallets?.updateWith(sensitiveInfo) } updateWallets { wallets -> wallets?.updateWith(sensitiveInfo) }
selectedUserWallet.value?.let {
trackSignInEvent(it, Basic.SignedIn.SignInType.Biometric)
}
} }
.doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) } .doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) }
} }
@ -373,7 +390,7 @@ internal class DefaultUserWalletsListRepository(
if (newUserWallet.walletId == oldUserWallet.walletId && if (newUserWallet.walletId == oldUserWallet.walletId &&
oldUserWallet is UserWallet.Hot && newUserWallet is UserWallet.Cold oldUserWallet is UserWallet.Hot && newUserWallet is UserWallet.Cold
) { ) {
removeHotWalletsFromSDK(walletIds = listOf(oldUserWallet.walletId)) removeHotWalletsFromSDKAndRepos(walletIds = listOf(oldUserWallet.walletId))
// When upgrading from Hot to Cold, if biometric lock is available, set it // When upgrading from Hot to Cold, if biometric lock is available, set it
if (hasBiometry()) { if (hasBiometry()) {
setLock(newUserWallet.walletId, LockMethod.Biometric, changeUnsecured = true) setLock(newUserWallet.walletId, LockMethod.Biometric, changeUnsecured = true)
@ -384,13 +401,14 @@ internal class DefaultUserWalletsListRepository(
} }
} }
private suspend fun removeHotWalletsFromSDK(walletIds: List<UserWalletId>) { private suspend fun removeHotWalletsFromSDKAndRepos(walletIds: List<UserWalletId>) {
val hotWalletsToDelete = userWalletsSync() val hotWalletsToDelete = userWalletsSync()
.filterIsInstance<UserWallet.Hot>() .filterIsInstance<UserWallet.Hot>()
.filter { walletIds.contains(it.walletId) } .filter { walletIds.contains(it.walletId) }
hotWalletsToDelete.forEach { hotWalletsToDelete.forEach { wallet ->
tangemHotSdk.delete(it.hotWalletId) hotWalletRepository.setAccessCodeSkipped(wallet.walletId, false) // In case the wallet is added again
tangemHotSdk.delete(wallet.hotWalletId)
} }
} }
@ -508,4 +526,14 @@ internal class DefaultUserWalletsListRepository(
return lastOrNull() return lastOrNull()
} }
private fun trackSignInEvent(userWallet: UserWallet, type: Basic.SignedIn.SignInType) {
trackingContextProxy.addContext(userWallet)
analyticsEventHandler.send(
event = Basic.SignedIn(
signInType = type,
walletsCount = userWallets.value?.size ?: 0,
),
)
}
} }

View file

@ -4,6 +4,7 @@ import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.Types import com.squareup.moshi.Types
import com.tangem.common.authentication.storage.AuthenticatedStorage import com.tangem.common.authentication.storage.AuthenticatedStorage
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.secure.SecureStorage import com.tangem.common.services.secure.SecureStorage
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.hot.sdk.android.crypto.AESEncryptionProtocol import com.tangem.hot.sdk.android.crypto.AESEncryptionProtocol
@ -96,7 +97,13 @@ internal class UserWalletEncryptionKeysRepository(
StorageKey.UserWalletEncryptionKey(userWalletId).name StorageKey.UserWalletEncryptionKey(userWalletId).name
} }
authenticatedStorage.get(keys).mapNotNull { val result = authenticatedStorage.get(keys)
if (keys.isNotEmpty() && result.isEmpty()) {
throw TangemSdkError.KeystoreInvalidated(Exception("Keys is empty"))
}
result.mapNotNull {
it.value.decodeToKey() it.value.decodeToKey()
} }
} }

View file

@ -196,7 +196,7 @@ class DetailsMiddleware {
} }
private fun enrollBiometrics() { private fun enrollBiometrics() {
Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication) Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication())
store.inject(DaggerGraphState::settingsManager).openBiometricSettings() store.inject(DaggerGraphState::settingsManager).openBiometricSettings()
} }

View file

@ -25,7 +25,7 @@ internal class AppSettingsItemsAnalyticsSender @Inject constructor(
private fun getEvent(item: AppSettingsScreenState.Item): AnalyticsEvent? { private fun getEvent(item: AppSettingsScreenState.Item): AnalyticsEvent? {
return when (item.id) { return when (item.id) {
AppSettingsItemsFactory.ID_ENROLL_BIOMETRICS_CARD -> Settings.AppSettings.EnableBiometrics AppSettingsItemsFactory.ID_ENROLL_BIOMETRICS_CARD -> Settings.AppSettings.EnableBiometrics()
else -> null else -> null
} }
} }

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
@ -24,6 +25,9 @@ import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R import com.tangem.wallet.R
private const val CARD_PLACEHOLDER_SECONDARY_ROTATION = -15f
private const val CARD_PLACEHOLDER_PRIMARY_ROTATION = -1f
@Composable @Composable
internal fun CardSettingsScreen(state: CardSettingsScreenState, modifier: Modifier = Modifier) { internal fun CardSettingsScreen(state: CardSettingsScreenState, modifier: Modifier = Modifier) {
val isCardReadingNeeded = state.cardDetails == null val isCardReadingNeeded = state.cardDetails == null
@ -42,74 +46,82 @@ internal fun CardSettingsScreen(state: CardSettingsScreenState, modifier: Modifi
) )
} }
@Suppress("MagicNumber")
@Composable @Composable
private fun CardSettingsReadCard(onScanCardClick: () -> Unit) { private fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) { ) {
Box( CardPlaceholderImages()
modifier = Modifier
.fillMaxWidth()
.padding(bottom = TangemTheme.dimens.spacing40)
.testTag(DeviceSettingsScreenTestTags.IMAGE_BLOCK),
) {
Image(
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing80,
end = TangemTheme.dimens.spacing80,
top = TangemTheme.dimens.spacing70,
)
.rotate(-15f),
painter = painterResource(id = R.drawable.card_placeholder_secondary),
contentDescription = null,
contentScale = ContentScale.FillWidth,
)
Image(
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing60,
end = TangemTheme.dimens.spacing60,
)
.rotate(-1f),
painter = painterResource(id = R.drawable.card_placeholder_black),
contentDescription = null,
contentScale = ContentScale.FillWidth,
)
}
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Column( ScanCardContent(onScanCardClick = onScanCardClick)
}
}
@Composable
private fun CardPlaceholderImages() {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = TangemTheme.dimens.spacing40)
.testTag(DeviceSettingsScreenTestTags.IMAGE_BLOCK),
) {
Image(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding( .padding(
start = TangemTheme.dimens.spacing16, start = TangemTheme.dimens.spacing80,
end = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing80,
bottom = TangemTheme.dimens.spacing32, top = TangemTheme.dimens.spacing70,
), )
) { .rotate(CARD_PLACEHOLDER_SECONDARY_ROTATION),
Text( painter = painterResource(id = R.drawable.card_placeholder_secondary),
text = stringResourceSafe(id = R.string.scan_card_settings_title), contentDescription = null,
color = TangemTheme.colors.text.primary1, contentScale = ContentScale.FillWidth,
style = TangemTheme.typography.h3, )
) Image(
Spacer(modifier = Modifier.size(TangemTheme.dimens.size20)) modifier = Modifier
Text( .fillMaxWidth()
text = stringResourceSafe(id = R.string.scan_card_settings_message), .padding(
color = TangemTheme.colors.text.secondary, start = TangemTheme.dimens.spacing60,
style = TangemTheme.typography.body1, end = TangemTheme.dimens.spacing60,
modifier = Modifier )
.verticalScroll(rememberScrollState()) .rotate(CARD_PLACEHOLDER_PRIMARY_ROTATION),
.weight(weight = 1f, fill = false), painter = painterResource(id = R.drawable.card_placeholder_black),
) contentDescription = null,
Spacer(modifier = Modifier.size(TangemTheme.dimens.size32)) contentScale = ContentScale.FillWidth,
DetailsMainButton( )
title = stringResourceSafe(id = R.string.scan_card_settings_button), }
onClick = onScanCardClick, }
)
} @Composable
private fun ScanCardContent(onScanCardClick: () -> Unit) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing32,
),
) {
Text(
text = stringResourceSafe(id = R.string.scan_card_settings_title),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h3,
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size20))
Text(
text = stringResourceSafe(id = R.string.scan_card_settings_message),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body1,
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size32))
DetailsMainButton(
title = stringResourceSafe(id = R.string.scan_card_settings_button),
onClick = onScanCardClick,
)
} }
} }

View file

@ -1,9 +1,7 @@
package com.tangem.tap.features.details.ui.cardsettings package com.tangem.tap.features.details.ui.cardsettings
import androidx.annotation.StringRes import com.tangem.core.ui.extensions.TextReference
import androidx.compose.runtime.Composable import com.tangem.core.ui.extensions.wrappedList
import androidx.compose.runtime.ReadOnlyComposable
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.tap.features.details.redux.SecurityOption import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.ui.securitymode.toTitleRes import com.tangem.tap.features.details.ui.securitymode.toTitleRes
import com.tangem.wallet.R import com.tangem.wallet.R
@ -32,7 +30,7 @@ internal sealed class CardInfo(
class SignedHashes(hashes: String) : CardInfo( class SignedHashes(hashes: String) : CardInfo(
titleRes = TextReference.Res(R.string.details_row_title_signed_hashes), titleRes = TextReference.Res(R.string.details_row_title_signed_hashes),
subtitle = TextReference.Res(R.string.details_row_subtitle_signed_hashes_format, hashes), subtitle = TextReference.Res(R.string.details_row_subtitle_signed_hashes_format, wrappedList(hashes)),
) )
class SecurityMode(securityOption: SecurityOption, clickable: Boolean) : CardInfo( class SecurityMode(securityOption: SecurityOption, clickable: Boolean) : CardInfo(
@ -47,7 +45,7 @@ internal sealed class CardInfo(
isClickable = true, isClickable = true,
) )
class AccessCodeRecovery(val isEnabled: Boolean) : CardInfo( class AccessCodeRecovery(isEnabled: Boolean) : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_access_code_recovery_title), titleRes = TextReference.Res(R.string.card_settings_access_code_recovery_title),
subtitle = if (isEnabled) { subtitle = if (isEnabled) {
TextReference.Res(R.string.common_enabled) TextReference.Res(R.string.common_enabled)
@ -62,22 +60,4 @@ internal sealed class CardInfo(
subtitle = description, subtitle = description,
isClickable = true, isClickable = true,
) )
}
// TODO("Remove and use the same from coreUI")
internal sealed interface TextReference {
class Res(@StringRes val id: Int, val formatArgs: List<Any> = emptyList()) : TextReference {
constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList())
}
class Str(val value: String) : TextReference
}
@Composable
@ReadOnlyComposable
internal fun TextReference.resolveReference(): String {
return when (this) {
is TextReference.Res -> stringResourceSafe(this.id, *this.formatArgs.toTypedArray())
is TextReference.Str -> this.value
}
} }

View file

@ -1,7 +1,7 @@
package com.tangem.tap.features.details.ui.common.utils package com.tangem.tap.features.details.ui.common.utils
import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.CardTypesResolver
import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.wallet.R import com.tangem.wallet.R
internal fun getResetToFactoryDescription( internal fun getResetToFactoryDescription(

View file

@ -17,8 +17,8 @@ import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.ResetCardScreenTestTags import com.tangem.core.ui.test.ResetCardScreenTestTags
import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.tap.features.details.ui.cardsettings.resolveReference import com.tangem.core.ui.extensions.resolveReference
import com.tangem.tap.features.details.ui.common.DetailsMainButton import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R import com.tangem.wallet.R
@ -198,7 +198,7 @@ private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) {
} }
@Composable @Composable
private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) { private fun CommonResetDialog(dialog: ResetCardDialog) {
BasicDialog( BasicDialog(
title = stringResourceSafe(dialog.titleResId), title = stringResourceSafe(dialog.titleResId),
message = stringResourceSafe(dialog.messageResId), message = stringResourceSafe(dialog.messageResId),

View file

@ -1,7 +1,7 @@
package com.tangem.tap.features.details.ui.resetcard package com.tangem.tap.features.details.ui.resetcard
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.wallet.R import com.tangem.wallet.R
internal data class ResetCardScreenState( internal data class ResetCardScreenState(

View file

@ -18,12 +18,12 @@ object UnfinishedBackupFoundDialog {
setTitle(R.string.common_warning) setTitle(R.string.common_warning)
setMessage(R.string.welcome_interrupted_backup_alert_message) setMessage(R.string.welcome_interrupted_backup_alert_message)
setPositiveButton(R.string.welcome_interrupted_backup_alert_resume) { _, _ -> setPositiveButton(R.string.welcome_interrupted_backup_alert_resume) { _, _ ->
Analytics.send(OnboardingEvent.Backup.ResumeInterruptedBackup) Analytics.send(OnboardingEvent.Backup.ResumeInterruptedBackup())
store.dispatch(GlobalAction.HideDialog) store.dispatch(GlobalAction.HideDialog)
store.dispatch(BackupAction.ResumeFoundUnfinishedBackup(scanResponse)) store.dispatch(BackupAction.ResumeFoundUnfinishedBackup(scanResponse))
} }
setNegativeButton(R.string.welcome_interrupted_backup_alert_discard) { _, _ -> setNegativeButton(R.string.welcome_interrupted_backup_alert_discard) { _, _ ->
Analytics.send(OnboardingEvent.Backup.CancelInterruptedBackup) Analytics.send(OnboardingEvent.Backup.CancelInterruptedBackup())
store.dispatch(GlobalAction.HideDialog) store.dispatch(GlobalAction.HideDialog)
store.dispatch(GlobalAction.ShowDialog(BackupDialog.ConfirmDiscardingBackup(scanResponse))) store.dispatch(GlobalAction.ShowDialog(BackupDialog.ConfirmDiscardingBackup(scanResponse)))
} }

View file

@ -10,7 +10,7 @@ import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.tap.common.analytics.events.SignIn import com.tangem.tap.common.analytics.events.SignIn
import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.tap.features.welcome.component.WelcomeComponent import com.tangem.tap.features.welcome.component.WelcomeComponent
import com.tangem.tap.features.welcome.redux.WelcomeAction import com.tangem.tap.features.welcome.redux.WelcomeAction
import com.tangem.tap.features.welcome.redux.WelcomeState import com.tangem.tap.features.welcome.redux.WelcomeState

View file

@ -58,7 +58,7 @@ internal class WelcomeMiddleware {
.doOnSuccess { selectedUserWallet -> .doOnSuccess { selectedUserWallet ->
sendSignedInAnalyticsEvent( sendSignedInAnalyticsEvent(
userWallet = selectedUserWallet, userWallet = selectedUserWallet,
signInType = Basic.SignedIn.SignInType.Biometric, signInType = Basic.SignedInLegacy.SignInType.Biometric,
) )
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) } store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
@ -80,7 +80,7 @@ internal class WelcomeMiddleware {
store.dispatchWithMain(WelcomeAction.ProceedWithCard.Error(error)) store.dispatchWithMain(WelcomeAction.ProceedWithCard.Error(error))
} }
.doOnSuccess { .doOnSuccess {
sendSignedInAnalyticsEvent(userWallet, signInType = Basic.SignedIn.SignInType.Card) sendSignedInAnalyticsEvent(userWallet, signInType = Basic.SignedInLegacy.SignInType.Card)
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) } store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
store.dispatchWithMain(WelcomeAction.ProceedWithCard.Success) store.dispatchWithMain(WelcomeAction.ProceedWithCard.Success)
@ -89,9 +89,7 @@ internal class WelcomeMiddleware {
} }
} }
private fun sendSignedInAnalyticsEvent(userWallet: UserWallet, signInType: Basic.SignedIn.SignInType) { private fun sendSignedInAnalyticsEvent(userWallet: UserWallet, signInType: Basic.SignedInLegacy.SignInType) {
// TODO [REDACTED_TASK_KEY] [Hot Wallet] Analytics
if (userWallet !is UserWallet.Cold) { if (userWallet !is UserWallet.Cold) {
return return
} }
@ -108,7 +106,7 @@ internal class WelcomeMiddleware {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
Analytics.send( Analytics.send(
event = Basic.SignedIn( event = Basic.SignedInLegacy(
currency = currency, currency = currency,
batch = scanResponse.card.batchId, batch = scanResponse.card.batchId,
signInType = signInType, signInType = signInType,

View file

@ -1,6 +1,6 @@
package com.tangem.tap.features.welcome.ui package com.tangem.tap.features.welcome.ui
import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.tap.features.welcome.ui.model.WarningModel import com.tangem.tap.features.welcome.ui.model.WarningModel
internal data class WelcomeScreenState( internal data class WelcomeScreenState(

View file

@ -18,8 +18,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.tap.features.details.ui.cardsettings.resolveReference import com.tangem.core.ui.extensions.resolveReference
import com.tangem.tap.features.welcome.component.WelcomeComponent import com.tangem.tap.features.welcome.component.WelcomeComponent
import com.tangem.tap.features.welcome.component.impl.PreviewWelcomeComponent import com.tangem.tap.features.welcome.component.impl.PreviewWelcomeComponent
import com.tangem.tap.features.welcome.ui.WelcomeScreenState import com.tangem.tap.features.welcome.ui.WelcomeScreenState

View file

@ -51,7 +51,9 @@ internal class DefaultAuthProvider(
ApiEnvironment.DEV_2, ApiEnvironment.DEV_2,
ApiEnvironment.DEV_3, ApiEnvironment.DEV_3,
-> environmentConfigStorage.getConfigSync().tangemApiKeyDev -> environmentConfigStorage.getConfigSync().tangemApiKeyDev
ApiEnvironment.STAGE -> environmentConfigStorage.getConfigSync().tangemApiKeyStage ApiEnvironment.STAGE_2,
ApiEnvironment.STAGE,
-> environmentConfigStorage.getConfigSync().tangemApiKeyStage
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().tangemApiKey ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().tangemApiKey
} ?: error("No tangem tech api config provided") } ?: error("No tangem tech api config provided")
} }

View file

@ -1,6 +1,7 @@
package com.tangem.tap.network.auth package com.tangem.tap.network.auth
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
import com.tangem.lib.auth.P2PEthPoolAuthProvider import com.tangem.lib.auth.P2PEthPoolAuthProvider
internal class DefaultP2PEthPoolAuthProvider( internal class DefaultP2PEthPoolAuthProvider(
@ -11,6 +12,6 @@ internal class DefaultP2PEthPoolAuthProvider(
val keys = environmentConfigStorage.getConfigSync().p2pApiKey val keys = environmentConfigStorage.getConfigSync().p2pApiKey
?: error("No P2P api keys provided") ?: error("No P2P api keys provided")
return keys.mainnet return if (P2PStakingConfig.USE_TESTNET) keys.hoodi else keys.mainnet
} }
} }

View file

@ -11,8 +11,11 @@ import com.arkivanov.essenty.lifecycle.subscribe
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.entity.InitScreenLaunchMode import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.context.childByContext
@ -26,6 +29,7 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.features.hotwallet.HotAccessCodeRequestComponent import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy
import com.tangem.features.walletconnect.components.WcRoutingComponent import com.tangem.features.walletconnect.components.WcRoutingComponent
import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.TangemHotSdk
@ -63,6 +67,9 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
private val userWalletsListRepository: UserWalletsListRepository, private val userWalletsListRepository: UserWalletsListRepository,
private val cardRepository: CardRepository, private val cardRepository: CardRepository,
private val onboardingRepository: OnboardingRepository, private val onboardingRepository: OnboardingRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val trackingContextProxy: TrackingContextProxy,
private val analyticsEventHandler: AnalyticsEventHandler,
private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val analyticsExceptionHandler: AnalyticsExceptionHandler,
) : RoutingComponent, ) : RoutingComponent,
AppComponentContext by context, AppComponentContext by context,
@ -151,6 +158,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
) )
} }
else -> { else -> {
trackSignInEvent()
AppRoute.Wallet AppRoute.Wallet
} }
}.also { }.also {
@ -235,4 +243,18 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound(onboardingScanResponse))) store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound(onboardingScanResponse)))
} }
} }
private suspend fun trackSignInEvent() {
if (hotWalletFeatureToggles.isHotWalletEnabled) {
val userWallets = userWalletsListRepository.userWalletsSync()
val selectedWallet = userWalletsListRepository.selectedUserWalletSync() ?: return
trackingContextProxy.addContext(selectedWallet)
analyticsEventHandler.send(
event = Basic.SignedIn(
signInType = Basic.SignedIn.SignInType.NoSecurity,
walletsCount = userWallets.size,
),
)
}
}
} }

View file

@ -14,7 +14,6 @@ object RoutingTransitionAnimationFactory {
@Suppress("MagicNumber") @Suppress("MagicNumber")
fun create(appRoute: AppRoute): StackAnimator { fun create(appRoute: AppRoute): StackAnimator {
return when (appRoute) { return when (appRoute) {
is AppRoute.Onboarding,
is AppRoute.Welcome, is AppRoute.Welcome,
is AppRoute.Home, is AppRoute.Home,
-> fade(tween(400)).plus(scale(tween(400))) -> fade(tween(400)).plus(scale(tween(400)))

View file

@ -529,7 +529,9 @@ internal class ChildFactory @Inject constructor(
is AppRoute.CreateMobileWallet -> { is AppRoute.CreateMobileWallet -> {
createComponentChild( createComponentChild(
context = context, context = context,
params = Unit, params = CreateMobileWalletComponent.Params(
source = route.source,
),
componentFactory = createMobileWalletComponentFactory, componentFactory = createMobileWalletComponentFactory,
) )
} }
@ -565,7 +567,7 @@ internal class ChildFactory @Inject constructor(
params = CreateWalletBackupComponent.Params( params = CreateWalletBackupComponent.Params(
userWalletId = route.userWalletId, userWalletId = route.userWalletId,
isUpgradeFlow = route.isUpgradeFlow, isUpgradeFlow = route.isUpgradeFlow,
shouldSetAccessCode = route.setAccessCode, shouldSetAccessCode = route.shouldSetAccessCode,
analyticsSource = route.analyticsSource, analyticsSource = route.analyticsSource,
analyticsAction = route.analyticsAction, analyticsAction = route.analyticsAction,
), ),

View file

@ -1,4 +1,5 @@
import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import java.util.concurrent.ConcurrentHashMap
plugins { plugins {
alias(deps.plugins.kotlin.android) apply false alias(deps.plugins.kotlin.android) apply false
@ -32,21 +33,53 @@ interface Injected {
val fs: FileSystemOperations val fs: FileSystemOperations
} }
// Test Logging data class TestStats(
val total: Long = 0,
val passed: Long = 0,
val failed: Long = 0,
val skipped: Long = 0,
)
val testResultsByModule = ConcurrentHashMap<String, TestStats>()
// Test task to run unit tests for debug/googleDebug variant (Android) and all JVM modules
val unitTest by tasks.registering {
group = "verification"
description = "Run unit tests for debug/googleDebug variant and all JVM modules"
doLast {
if (testResultsByModule.isNotEmpty()) {
val totalStats = testResultsByModule.values.fold(TestStats()) { acc, stats ->
TestStats(
total = acc.total + stats.total,
passed = acc.passed + stats.passed,
failed = acc.failed + stats.failed,
skipped = acc.skipped + stats.skipped,
)
}
println("\n" + "=".repeat(80))
println("TEST SUMMARY")
println("=".repeat(80))
testResultsByModule.toSortedMap().forEach { (module, stats) ->
println(" $module: ${stats.total} tests (${stats.passed} passed, ${stats.failed} failed, ${stats.skipped} skipped)")
}
println("-".repeat(80))
println("TOTAL: ${totalStats.total} tests in ${testResultsByModule.size} modules")
println(" Passed: ${totalStats.passed}")
println(" Failed: ${totalStats.failed}")
println(" Skipped: ${totalStats.skipped}")
println("=".repeat(80))
}
}
}
// Test Logging and testCI dependencies
subprojects { subprojects {
tasks.withType<Test>().configureEach { tasks.withType<Test>().configureEach {
val taskName = name.lowercase() println("Test task scheduled: $path")
if (taskName.contains("external") ||
taskName.contains("internal") ||
taskName.contains("release") ||
taskName.contains("mocked") ||
taskName.contains("huawei")
) {
enabled = false
println("Skipping test task: $name")
} else {
println("Test task scheduled: $name")
}
testLogging { testLogging {
exceptionFormat = TestExceptionFormat.FULL exceptionFormat = TestExceptionFormat.FULL
@ -54,6 +87,13 @@ subprojects {
afterSuite(KotlinClosure2<TestDescriptor, TestResult, Unit>({ desc, result -> afterSuite(KotlinClosure2<TestDescriptor, TestResult, Unit>({ desc, result ->
if (desc.parent == null) { // will match the outermost suite if (desc.parent == null) { // will match the outermost suite
testResultsByModule[path] = TestStats(
total = result.testCount,
passed = result.successfulTestCount,
failed = result.failedTestCount,
skipped = result.skippedTestCount,
)
val output = val output =
"Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)"
val startItem = "| " val startItem = "| "
@ -68,6 +108,28 @@ subprojects {
})) }))
} }
} }
// Register testCI dependencies
// App module
plugins.withId("com.android.application") {
afterEvaluate {
unitTest.configure { dependsOn(tasks.named("testGoogleDebugUnitTest")) }
}
}
// Android libraries
plugins.withId("com.android.library") {
afterEvaluate {
unitTest.configure { dependsOn(tasks.named("testDebugUnitTest")) }
}
}
// Jvm modules
plugins.withId("org.jetbrains.kotlin.jvm") {
if (!plugins.hasPlugin("com.android.library") && !plugins.hasPlugin("com.android.application")) {
unitTest.configure { dependsOn(tasks.named("test")) }
}
}
} }
val assembleInternalQA by tasks.registering { val assembleInternalQA by tasks.registering {

View file

@ -1,9 +0,0 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:AppRoute.kt$AppRoute.CreateWalletBackup$val setAccessCode: Boolean = false</ID>
<ID>NestedScopeFunctions:PayloadToDeeplinkConverter.kt$PayloadToDeeplinkConverter$let { addQueryParam(NAME_KEY, it) }</ID>
<ID>NestedScopeFunctions:PayloadToDeeplinkConverter.kt$PayloadToDeeplinkConverter$let { addQueryParam(TRANSACTION_ID_KEY, it) }</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -346,7 +346,9 @@ sealed class AppRoute(val path: String) : Route {
object CreateHardwareWallet : AppRoute(path = "/create_hardware_wallet") object CreateHardwareWallet : AppRoute(path = "/create_hardware_wallet")
@Serializable @Serializable
object CreateMobileWallet : AppRoute(path = "/create_mobile_wallet") data class CreateMobileWallet(
val source: String,
) : AppRoute(path = "/create_mobile_wallet")
@Serializable @Serializable
data class UpgradeWallet( data class UpgradeWallet(
@ -368,7 +370,7 @@ sealed class AppRoute(val path: String) : Route {
val analyticsSource: String, val analyticsSource: String,
val analyticsAction: String, val analyticsAction: String,
val isUpgradeFlow: Boolean = false, val isUpgradeFlow: Boolean = false,
val setAccessCode: Boolean = false, val shouldSetAccessCode: Boolean = false,
) : AppRoute(path = "/create_wallet_backup/${userWalletId.stringValue}") ) : AppRoute(path = "/create_wallet_backup/${userWalletId.stringValue}")
@Serializable @Serializable

View file

@ -55,8 +55,12 @@ object PayloadToDeeplinkConverter : Converter<Map<String, String>, String?> {
addQueryParam(DERIVATION_PATH_KEY, derivationPath) addQueryParam(DERIVATION_PATH_KEY, derivationPath)
} }
transactionId?.let { addQueryParam(TRANSACTION_ID_KEY, it) } if (transactionId != null) {
name?.let { addQueryParam(NAME_KEY, it) } addQueryParam(TRANSACTION_ID_KEY, transactionId)
}
if (name != null) {
addQueryParam(NAME_KEY, name)
}
}.build() }.build()
} }

View file

@ -0,0 +1,82 @@
package com.tangem.common.test.data.staking
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolExitQueueDTO
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolStakeDTO
import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import java.math.BigDecimal
/**
* Factory for creating mock P2P ETH Pool account responses for testing
*/
object MockP2PEthPoolAccountResponseFactory {
private val defaultStakingId = StakingID(
integrationId = "p2p-ethereum-pooled",
address = "0x5aa711F440Eb6d4361148bBD89d03464628ace84",
)
const val defaultVaultAddress = "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
fun createWithBalance(
stakingId: StakingID = defaultStakingId,
vaultAddress: String = defaultVaultAddress,
stakedAmount: BigDecimal = BigDecimal("1.5"),
earnedAmount: BigDecimal = BigDecimal("0.05"),
): P2PEthPoolAccountResponse {
return P2PEthPoolAccountResponse(
delegatorAddress = stakingId.address,
vaultAddress = vaultAddress,
stake = P2PEthPoolStakeDTO(
assets = stakedAmount,
totalEarnedAssets = earnedAmount,
),
availableToUnstake = stakedAmount,
availableToWithdraw = BigDecimal.ZERO,
exitQueue = P2PEthPoolExitQueueDTO(
total = 0.0,
requests = emptyList(),
),
)
}
fun createWithEmptyBalance(
stakingId: StakingID = defaultStakingId,
vaultAddress: String = defaultVaultAddress,
): P2PEthPoolAccountResponse {
return P2PEthPoolAccountResponse(
delegatorAddress = stakingId.address,
vaultAddress = vaultAddress,
stake = P2PEthPoolStakeDTO(
assets = BigDecimal.ZERO,
totalEarnedAssets = BigDecimal.ZERO,
),
availableToUnstake = BigDecimal.ZERO,
availableToWithdraw = BigDecimal.ZERO,
exitQueue = P2PEthPoolExitQueueDTO(
total = 0.0,
requests = emptyList(),
),
)
}
fun createMockVault(vaultAddress: String = defaultVaultAddress): P2PEthPoolVault {
return P2PEthPoolVault(
vaultAddress = vaultAddress,
displayName = "Test Vault",
apy = BigDecimal("3.5"),
baseApy = BigDecimal("3.0"),
capacity = BigDecimal("10000"),
totalAssets = BigDecimal("5000"),
feePercent = BigDecimal("10"),
isPrivate = false,
isGenesis = false,
isSmoothingPool = false,
isErc20 = false,
tokenName = "Test Token",
tokenSymbol = "TT",
createdAt = 0L,
)
}
}

View file

@ -18,9 +18,7 @@
<ID>CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$minimumSendAmount: BigDecimal?</ID> <ID>CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$minimumSendAmount: BigDecimal?</ID>
<ID>CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$rentWarning: CryptoCurrencyWarning.Rent?</ID> <ID>CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$rentWarning: CryptoCurrencyWarning.Rent?</ID>
<ID>MultilineLambdaItParameter:ExpressStatusItems.kt${ val itemInfo = expressTxs[it].info val (iconRes, tint) = when (itemInfo.iconState) { ExpressTransactionStateIconUM.Warning -&gt; { R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention } ExpressTransactionStateIconUM.Error -&gt; { R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning } ExpressTransactionStateIconUM.None -&gt; null to null } ExpressStatusItem( title = itemInfo.title, fromTokenIconState = itemInfo.fromCurrencyIcon, toTokenIconState = itemInfo.toCurrencyIcon, fromAmount = itemInfo.fromAmount, fromSymbol = itemInfo.fromAmountSymbol, toAmount = itemInfo.toAmount, toSymbol = itemInfo.toAmountSymbol, onClick = itemInfo.onClick, infoIconRes = iconRes, infoIconTint = tint, modifier = modifier.animateItem(), ) }</ID> <ID>MultilineLambdaItParameter:ExpressStatusItems.kt${ val itemInfo = expressTxs[it].info val (iconRes, tint) = when (itemInfo.iconState) { ExpressTransactionStateIconUM.Warning -&gt; { R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention } ExpressTransactionStateIconUM.Error -&gt; { R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning } ExpressTransactionStateIconUM.None -&gt; null to null } ExpressStatusItem( title = itemInfo.title, fromTokenIconState = itemInfo.fromCurrencyIcon, toTokenIconState = itemInfo.toCurrencyIcon, fromAmount = itemInfo.fromAmount, fromSymbol = itemInfo.fromAmountSymbol, toAmount = itemInfo.toAmount, toSymbol = itemInfo.toAmountSymbol, onClick = itemInfo.onClick, infoIconRes = iconRes, infoIconTint = tint, modifier = modifier.animateItem(), ) }</ID>
<ID>MultilineLambdaItParameter:TokenItemStateConverter.kt$TokenItemStateConverter${ createTitleState(it, yieldModuleApyMap, stakingApyMap, { onApyLabelClick?.invoke(it) }) }</ID>
<ID>MultilineLambdaItParameter:TokenItemStateConverter.kt$TokenItemStateConverter.Companion${ it.key.equals( other = token.yieldSupplyKey(), ignoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId), ) }</ID> <ID>MultilineLambdaItParameter:TokenItemStateConverter.kt$TokenItemStateConverter.Companion${ it.key.equals( other = token.yieldSupplyKey(), ignoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId), ) }</ID>
<ID>NamedArguments:TokenItemStateConverter.kt$TokenItemStateConverter$createTitleState(it, yieldModuleApyMap, stakingApyMap, { onApyLabelClick?.invoke(it) })</ID>
<ID>NoNameShadowing:NavigationButtonsBlock.kt$navigationUM</ID> <ID>NoNameShadowing:NavigationButtonsBlock.kt$navigationUM</ID>
<ID>NoNameShadowing:UserWalletItem.kt$balance</ID> <ID>NoNameShadowing:UserWalletItem.kt$balance</ID>
<ID>NullableBooleanCheck:TokenItemStateConverter.kt$TokenItemStateConverter.Companion$cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive ?: false</ID> <ID>NullableBooleanCheck:TokenItemStateConverter.kt$TokenItemStateConverter.Companion$cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive ?: false</ID>

View file

@ -271,6 +271,7 @@ private fun AmountFieldError(
style = TangemTheme.typography.caption2, style = TangemTheme.typography.caption2,
color = color, color = color,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.testTag(SendScreenTestTags.AMOUNT_ERROR_TEXT),
) )
} }
} }

View file

@ -1,76 +0,0 @@
package com.tangem.common.ui.news
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Composable
internal fun ArticleBadge(articleTagUM: ArticleTagUM, modifier: Modifier = Modifier) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier
.heightIn(min = 24.dp)
.background(
color = TangemTheme.colors.icon.informative.copy(alpha = 0.1f),
shape = RoundedCornerShape(8.dp),
)
.padding(horizontal = 8.dp, vertical = 4.dp),
) {
when (articleTagUM) {
is ArticleTagUM.Category -> Unit
is ArticleTagUM.Token -> {
CurrencyIcon(
state = articleTagUM.iconState,
shouldDisplayNetwork = false,
iconSize = 16.dp,
)
}
}
Text(
text = articleTagUM.title.resolveReference(),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.secondary,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ArticleBadgePreview() {
TangemThemePreview {
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
ArticleBadge(
articleTagUM = ArticleTagUM.Token(
TextReference.Str("BTC"),
iconState = CurrencyIconState.CoinIcon(
url = "",
fallbackResId = 0,
isGrayscale = false,
shouldShowCustomBadge = false,
),
),
)
ArticleBadge(
articleTagUM = ArticleTagUM.Category(TextReference.Str("Regulation")),
)
}
}
}

View file

@ -1,41 +1,47 @@
package com.tangem.common.ui.news package com.tangem.common.ui.news
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CardColors
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.utils.StringsSigns import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import kotlinx.collections.immutable.toPersistentList
@Composable @Composable
fun ArticleCard(articleConfigUM: ArticleConfigUM, onArticleClick: () -> Unit, modifier: Modifier = Modifier) { fun ArticleCard(
articleConfigUM: ArticleConfigUM,
onArticleClick: () -> Unit,
modifier: Modifier = Modifier,
colors: CardColors = TangemBlockCardColors,
) {
BlockCard( BlockCard(
modifier = modifier, modifier = modifier,
onClick = onArticleClick, onClick = onArticleClick,
colors = colors,
) { ) {
if (articleConfigUM.isTrending) { if (articleConfigUM.isTrending) {
TrendingArticle(articleConfigUM = articleConfigUM) TrendingArticle(articleConfigUM = articleConfigUM)
@ -121,54 +127,13 @@ private fun DefaultArticle(articleConfigUM: ArticleConfigUM) {
} }
} }
@Composable
private fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = Modifier) {
val dotColor = TangemTheme.colors.text.secondary
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
imageVector = ImageVector.vectorResource(R.drawable.ic_start_circle_12),
contentDescription = null,
)
Text(
text = score.toString(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
)
Spacer(
modifier = Modifier
.size(4.dp)
.drawWithCache {
val radius = size.minDimension / 2f
onDrawBehind {
drawCircle(
color = dotColor,
radius = radius,
)
}
},
)
Text(
text = createdAt,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
)
}
}
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun Tags(tags: ImmutableList<ArticleTagUM>, modifier: Modifier = Modifier) { private fun Tags(tags: ImmutableList<LabelUM>, modifier: Modifier = Modifier) {
val expandIndicator = remember { val expandIndicator = remember {
ContextualFlowRowOverflow.expandIndicator { ContextualFlowRowOverflow.expandIndicator {
val remainingItems = tags.size - shownItemCount val remainingItems = tags.size - shownItemCount
ArticleBadge(articleTagUM = ArticleTagUM.Category(TextReference.Str("${StringsSigns.PLUS}$remainingItems"))) Label(state = LabelUM(TextReference.Str("${StringsSigns.PLUS}$remainingItems")))
} }
} }
ContextualFlowRow( ContextualFlowRow(
@ -179,7 +144,7 @@ private fun Tags(tags: ImmutableList<ArticleTagUM>, modifier: Modifier = Modifie
maxLines = 1, maxLines = 1,
overflow = expandIndicator, overflow = expandIndicator,
) { index -> ) { index ->
ArticleBadge(articleTagUM = tags[index]) Label(state = tags[index])
} }
} }
@ -189,14 +154,14 @@ private fun Tags(tags: ImmutableList<ArticleTagUM>, modifier: Modifier = Modifie
private fun TagsPreview() { private fun TagsPreview() {
TangemThemePreview { TangemThemePreview {
Tags( Tags(
tags = listOf( tags = persistentListOf(
ArticleTagUM.Category(TextReference.Str("Hype")), LabelUM(TextReference.Str("Hype")),
ArticleTagUM.Category(TextReference.Str("BTC")), LabelUM(TextReference.Str("BTC")),
ArticleTagUM.Category(TextReference.Str("Supply")), LabelUM(TextReference.Str("Supply")),
ArticleTagUM.Category(TextReference.Str("Demand")), LabelUM(TextReference.Str("Demand")),
ArticleTagUM.Category(TextReference.Str("Best rate")), LabelUM(TextReference.Str("Best rate")),
ArticleTagUM.Category(TextReference.Str("Breaking news")), LabelUM(TextReference.Str("Breaking news")),
).toPersistentList(), ),
) )
} }
} }
@ -206,12 +171,11 @@ private fun TagsPreview() {
@Composable @Composable
private fun ArticleCardsPreview() { private fun ArticleCardsPreview() {
val tags = listOf( val tags = listOf(
ArticleTagUM.Category(TextReference.Str("Hype")), LabelUM(TextReference.Str("Hype")),
ArticleTagUM.Category(TextReference.Str("BTC")), LabelUM(TextReference.Str("BTC")),
ArticleTagUM.Category(TextReference.Str("Supply")), LabelUM(TextReference.Str("Supply")),
ArticleTagUM.Category(TextReference.Str("Demand")), LabelUM(TextReference.Str("Demand")),
ArticleTagUM.Category(TextReference.Str("Best rate")), LabelUM(TextReference.Str("Breaking news")),
ArticleTagUM.Category(TextReference.Str("Breaking news")),
).toImmutableSet() ).toImmutableSet()
val config = ArticleConfigUM( val config = ArticleConfigUM(

View file

@ -1,5 +1,6 @@
package com.tangem.common.ui.news package com.tangem.common.ui.news
import com.tangem.core.ui.components.label.entity.LabelUM
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
data class ArticleConfigUM( data class ArticleConfigUM(
@ -8,6 +9,6 @@ data class ArticleConfigUM(
val score: Float, val score: Float,
val createdAt: String, val createdAt: String,
val isTrending: Boolean, val isTrending: Boolean,
val tags: ImmutableSet<ArticleTagUM>, val tags: ImmutableSet<LabelUM>,
val isViewed: Boolean, val isViewed: Boolean,
) )

View file

@ -0,0 +1,55 @@
package com.tangem.common.ui.news
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.res.TangemTheme
import kotlinx.collections.immutable.ImmutableList
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ArticleHeader(
title: String,
createdAt: String,
score: Float,
tags: ImmutableList<LabelUM>,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
ArticleInfo(
score = score,
createdAt = createdAt,
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = title,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
if (tags.isNotEmpty()) {
Spacer(modifier = Modifier.height(20.dp))
FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
tags.forEach { tag ->
Label(
state = tag,
)
}
}
}
}
}

View file

@ -0,0 +1,55 @@
package com.tangem.common.ui.news
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = Modifier) {
val dotColor = TangemTheme.colors.text.secondary
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
imageVector = ImageVector.vectorResource(R.drawable.ic_start_circle_12),
contentDescription = null,
)
Text(
text = score.toString(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
)
Spacer(
modifier = Modifier
.size(4.dp)
.drawWithCache {
val radius = size.minDimension / 2f
onDrawBehind {
drawCircle(
color = dotColor,
radius = radius,
)
}
},
)
Text(
text = createdAt,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
)
}
}

View file

@ -0,0 +1,63 @@
package com.tangem.common.ui.news
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.res.TangemTheme
@Composable
fun TrendingLoadingArticle(modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier,
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp, horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
RectangleShimmer(modifier = Modifier.size(width = 96.dp, height = 24.dp), radius = 8.dp)
SpacerH(12.dp)
RectangleShimmer(modifier = Modifier.size(width = 285.dp, height = 18.dp), radius = 4.dp)
SpacerH(6.dp)
RectangleShimmer(modifier = Modifier.size(width = 190.dp, height = 18.dp), radius = 4.dp)
SpacerH(14.dp)
RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 18.dp), radius = 4.dp)
SpacerH(32.dp)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
}
}
}
}
@Composable
fun DefaultLoadingArticle() {
BlockCard(
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
) {
Column(modifier = Modifier.padding(12.dp)) {
RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 16.dp), radius = 4.dp)
SpacerH(12.dp)
RectangleShimmer(modifier = Modifier.size(width = 142.dp, height = 18.dp), radius = 4.dp)
SpacerH(6.dp)
RectangleShimmer(modifier = Modifier.size(width = 176.dp, height = 18.dp), radius = 4.dp)
SpacerH(6.dp)
RectangleShimmer(modifier = Modifier.size(width = 120.dp, height = 18.dp), radius = 4.dp)
SpacerH(16.dp)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp)
RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp)
}
}
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.common.ui.news
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
@Immutable
sealed interface ArticleTagUM {
val title: TextReference
data class Category(override val title: TextReference) : ArticleTagUM
data class Token(
override val title: TextReference,
val iconState: CurrencyIconState,
) : ArticleTagUM
}

View file

@ -21,7 +21,7 @@ import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.yieldSupplyKey import com.tangem.domain.models.currency.yieldSupplyKey
import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.staking.model.isStakingSupported import com.tangem.domain.staking.model.isStakingSupported
import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
@ -171,7 +171,7 @@ class TokenItemStateConverter(
return totalAmount.format { crypto(currency) } return totalAmount.format { crypto(currency) }
} }
private fun CryptoCurrencyStatus.getStakedBalance() = (value.yieldBalance as? YieldBalance.Data) private fun CryptoCurrencyStatus.getStakedBalance() = (value.stakingBalance as? StakingBalance.Data)
?.getTotalWithRewardsStakingBalance(blockchainId = currency.network.rawId).orZero() ?.getTotalWithRewardsStakingBalance(blockchainId = currency.network.rawId).orZero()
private fun createTitleState( private fun createTitleState(
@ -278,12 +278,13 @@ class TokenItemStateConverter(
val validators = stakingApyMap[stakingKey] val validators = stakingApyMap[stakingKey]
?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null) ?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null)
val yieldBalance = currencyStatus.value.yieldBalance val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data
val hasStakedBalance = yieldBalance is YieldBalance.Data val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit
val rateInfo: Pair<BigDecimal, Yield.RewardType?>? = if (hasStakedBalance) { val rateInfo: Pair<BigDecimal, Yield.RewardType?>? = if (stakeKitBalance != null) {
// StakeKit-specific: try to find rate from validator address
val validatorsByAddress = validators.associateBy { it.address } val validatorsByAddress = validators.associateBy { it.address }
yieldBalance.balance.items stakeKitBalance.balance.items
.mapNotNull { it.validatorAddress } .mapNotNull { it.validatorAddress }
.firstNotNullOfOrNull { address -> .firstNotNullOfOrNull { address ->
val validator = validatorsByAddress[address] val validator = validatorsByAddress[address]
@ -298,6 +299,8 @@ class TokenItemStateConverter(
} }
.maxByOrNull { it.first } .maxByOrNull { it.first }
} else { } else {
// P2P or no balance: use preferred validators
// TODO p2p
validators validators
.filter { it.preferred } .filter { it.preferred }
.mapNotNull { validator -> .mapNotNull { validator ->
@ -310,7 +313,7 @@ class TokenItemStateConverter(
return StakingLocalInfo( return StakingLocalInfo(
rate = rateInfo?.first, rate = rateInfo?.first,
isActive = hasStakedBalance, isActive = stakingBalance != null,
rewardType = rateInfo?.second, rewardType = rateInfo?.second,
) )
} }

View file

@ -1,6 +1,8 @@
package com.tangem.common.ui.userwallet package com.tangem.common.ui.userwallet
import com.tangem.common.ui.R import com.tangem.common.ui.R
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.SignIn
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
@ -13,6 +15,7 @@ import com.tangem.domain.common.wallets.error.UnlockWalletError.UnableToUnlock.R
inline fun UnlockWalletError.handle( inline fun UnlockWalletError.handle(
onAlreadyUnlocked: () -> Unit = {}, onAlreadyUnlocked: () -> Unit = {},
onUserCancelled: () -> Unit = {}, onUserCancelled: () -> Unit = {},
analyticsEventHandler: AnalyticsEventHandler,
noinline showMessage: (EventMessage) -> Unit, noinline showMessage: (EventMessage) -> Unit,
) { ) {
when (this) { when (this) {
@ -30,15 +33,20 @@ inline fun UnlockWalletError.handle(
// This should never happen in this flow, as we always check for the wallet existence before unlocking // This should never happen in this flow, as we always check for the wallet existence before unlocking
showMessage(SnackbarMessage(TextReference.Res(R.string.generic_error))) showMessage(SnackbarMessage(TextReference.Res(R.string.generic_error)))
} }
is UnlockWalletError.UnableToUnlock -> handleUnableToUnlock(this, showMessage) is UnlockWalletError.UnableToUnlock -> handleUnableToUnlock(this, analyticsEventHandler, showMessage)
} }
} }
fun handleUnableToUnlock(error: UnlockWalletError.UnableToUnlock, showDialog: (DialogMessage) -> Unit) { fun handleUnableToUnlock(
error: UnlockWalletError.UnableToUnlock,
analyticsEventHandler: AnalyticsEventHandler,
showDialog: (DialogMessage) -> Unit,
) {
val dialogMessage = when (error) { val dialogMessage = when (error) {
is UnlockWalletError.UnableToUnlock.WithReason -> { is UnlockWalletError.UnableToUnlock.WithReason -> {
when (error.reason) { when (error.reason) {
Reason.AllKeysInvalidated -> { Reason.AllKeysInvalidated -> {
analyticsEventHandler.send(SignIn.ErrorBiometricUpdated())
DialogMessage( DialogMessage(
title = resourceReference(R.string.biometric_updated_warning_title), title = resourceReference(R.string.biometric_updated_warning_title),
message = resourceReference(R.string.biometric_updated_warning_description), message = resourceReference(R.string.biometric_updated_warning_description),

View file

@ -0,0 +1,17 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>MultilineLambdaItParameter:TechAnalyticsEvent.kt$TechAnalyticsEvent.KeyboardIdentifier${ put("Package", it) put("GPUrl", "https://play.google.com/store/apps/details?id=$packageName") }</ID>
<ID>UseEmptyCounterpart:AnalyticsEvent.kt$AnalyticsEvent$mapOf()</ID>
<ID>UseEmptyCounterpart:Basic.kt$Basic$mapOf()</ID>
<ID>UseEmptyCounterpart:ExceptionAnalyticsEvent.kt$ExceptionAnalyticsEvent$mapOf()</ID>
<ID>UseEmptyCounterpart:MainScreenAnalyticsEvent.kt$MainScreenAnalyticsEvent$mapOf()</ID>
<ID>UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent$mapOf()</ID>
<ID>UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent.CreateWallet$mapOf()</ID>
<ID>UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent.Error$mapOf()</ID>
<ID>UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent.Onboarding$mapOf()</ID>
<ID>UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent.SeedPhrase$mapOf()</ID>
<ID>UseEmptyCounterpart:TechAnalyticsEvent.kt$TechAnalyticsEvent$mapOf()</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -11,11 +11,6 @@ sealed class AnalyticsParam {
companion object companion object
} }
sealed class TokenBalanceState(val value: String) {
data object Empty : TokenBalanceState("Empty")
data object Full : TokenBalanceState("Full")
}
sealed class RateApp(val value: String) { sealed class RateApp(val value: String) {
data object Liked : RateApp("Liked") data object Liked : RateApp("Liked")
data object Disliked : RateApp("Disliked") data object Disliked : RateApp("Disliked")
@ -83,12 +78,14 @@ sealed class AnalyticsParam {
data object Onboarding : ScreensSources("Onboarding") data object Onboarding : ScreensSources("Onboarding")
data object LongTap : ScreensSources("Long Tap") data object LongTap : ScreensSources("Long Tap")
data object Markets : ScreensSources("Markets") data object Markets : ScreensSources("Markets")
data object HotWallet : ScreensSources("Hot Wallet")
data object TangemPay : ScreensSources("Tangem Pay") data object TangemPay : ScreensSources("Tangem Pay")
data object WalletSettings : ScreensSources("Wallet Settings") data object WalletSettings : ScreensSources("Wallet Settings")
data object Upgrade : ScreensSources("Upgrade") data object Upgrade : ScreensSources("Upgrade")
data object HardwareWallet : ScreensSources("Hardware Wallet") data object HardwareWallet : ScreensSources("Hardware Wallet")
data object ImportWallet : ScreensSources("Import Wallet") data object ImportWallet : ScreensSources("Import Wallet")
data object CreateWalletIntro : ScreensSources("Create Wallet Intro")
data object AddNewWallet : ScreensSources("Add New Wallet")
data object CreateWallet : ScreensSources("Create Wallet")
} }
sealed class TxSentFrom(val value: String) { sealed class TxSentFrom(val value: String) {
@ -203,8 +200,9 @@ sealed class AnalyticsParam {
Pending(value = "Pending"), Pending(value = "Pending"),
} }
enum class EnsStatus(val value: String) { enum class EmptyFull(val value: String) {
EMPTY("Empty"), FULL("Full") Empty("Empty"),
Full("Full"),
} }
enum class ProductType(val value: String) { enum class ProductType(val value: String) {
@ -268,5 +266,6 @@ sealed class AnalyticsParam {
const val CHOSEN_TOKEN = "Token Chosen" const val CHOSEN_TOKEN = "Token Chosen"
const val ENS = "ENS" const val ENS = "ENS"
const val ENS_ADDRESS = "ENS Address" const val ENS_ADDRESS = "ENS Address"
const val ACCOUNT_DERIVATION_FROM = "Account Derivation (from)"
} }
} }

View file

@ -10,11 +10,11 @@ sealed class Basic(
) : Basic( ) : Basic(
event = "Card Was Scanned", event = "Card Was Scanned",
params = mapOf( params = mapOf(
AnalyticsParam.SOURCE to source.value, AnalyticsParam.Key.SOURCE to source.value,
), ),
) )
class SignedIn( class SignedInLegacy(
currency: AnalyticsParam.WalletType, currency: AnalyticsParam.WalletType,
batch: String, batch: String,
signInType: SignInType, signInType: SignInType,
@ -24,8 +24,8 @@ sealed class Basic(
) : Basic( ) : Basic(
event = "Signed in", event = "Signed in",
params = buildMap { params = buildMap {
put(AnalyticsParam.CURRENCY, currency.value) put(AnalyticsParam.Key.CURRENCY, currency.value)
put(AnalyticsParam.BATCH, batch) put(AnalyticsParam.Key.BATCH, batch)
put("Wallet Type", if (isImported) "Seed Phrase" else "Seedless") put("Wallet Type", if (isImported) "Seed Phrase" else "Seedless")
put("Sign in type", signInType.name) put("Sign in type", signInType.name)
put("Wallets Count", walletsCount) put("Wallets Count", walletsCount)
@ -39,10 +39,37 @@ sealed class Basic(
} }
} }
class SignedIn(
signInType: SignInType,
walletsCount: Int,
) : Basic(
event = "Signed in",
params = buildMap {
put("Sign in type", signInType.value)
put("Wallets Count", walletsCount.toString())
},
) {
enum class SignInType(val value: String) {
Card("Card"),
Biometric("Biometric"),
NoSecurity("No Security"),
AccessCode("Access Code"),
}
}
class ButtonBuy(
source: AnalyticsParam.ScreensSources,
) : Basic(
event = "Button - Buy",
params = buildMap {
put(AnalyticsParam.Key.SOURCE, source.value)
},
)
class ToppedUp(userWalletId: String, currency: AnalyticsParam.WalletType) : class ToppedUp(userWalletId: String, currency: AnalyticsParam.WalletType) :
Basic( Basic(
event = "Topped up", event = "Topped up",
params = mapOf(AnalyticsParam.CURRENCY to currency.value), params = mapOf(AnalyticsParam.Key.CURRENCY to currency.value),
), ),
OneTimeAnalyticsEvent { OneTimeAnalyticsEvent {
@ -53,16 +80,16 @@ sealed class Basic(
Basic( Basic(
event = "Transaction sent", event = "Transaction sent",
params = buildMap { params = buildMap {
this[AnalyticsParam.SOURCE] = sentFrom.value this[AnalyticsParam.Key.SOURCE] = sentFrom.value
if (sentFrom is AnalyticsParam.TxData) { if (sentFrom is AnalyticsParam.TxData) {
this[AnalyticsParam.BLOCKCHAIN] = sentFrom.blockchain this[AnalyticsParam.Key.BLOCKCHAIN] = sentFrom.blockchain
this[AnalyticsParam.TOKEN_PARAM] = sentFrom.token this[AnalyticsParam.Key.TOKEN_PARAM] = sentFrom.token
sentFrom.feeType?.value?.let { sentFrom.feeType?.value?.let {
this[AnalyticsParam.FEE_TYPE] = it this[AnalyticsParam.Key.FEE_TYPE] = it
} }
} }
if (sentFrom is AnalyticsParam.TxSentFrom.Approve) { if (sentFrom is AnalyticsParam.TxSentFrom.Approve) {
this[AnalyticsParam.PERMISSION_TYPE] = sentFrom.permissionType this[AnalyticsParam.Key.PERMISSION_TYPE] = sentFrom.permissionType
} }
this["Memo"] = memoType.name this["Memo"] = memoType.name
}, },
@ -79,7 +106,7 @@ sealed class Basic(
class ButtonSupport(source: AnalyticsParam.ScreensSources) : Basic( class ButtonSupport(source: AnalyticsParam.ScreensSources) : Basic(
event = "Request Support", event = "Request Support",
params = mapOf( params = mapOf(
AnalyticsParam.SOURCE to source.value, AnalyticsParam.Key.SOURCE to source.value,
), ),
) )
@ -89,7 +116,7 @@ sealed class Basic(
) : Basic( ) : Basic(
event = "Biometry Failed", event = "Biometry Failed",
params = mapOf( params = mapOf(
AnalyticsParam.SOURCE to source.value, AnalyticsParam.Key.SOURCE to source.value,
"Reason" to reason.value, "Reason" to reason.value,
), ),
) { ) {

View file

@ -36,26 +36,34 @@ sealed class MainScreenAnalyticsEvent(
}, },
) )
data object ButtonReceive : MainScreenAnalyticsEvent( class ButtonReceive : MainScreenAnalyticsEvent(
event = "Button - Receive", event = "Button - Receive",
) )
data object LimitsClicked : MainScreenAnalyticsEvent( class LimitsClicked : MainScreenAnalyticsEvent(
event = "Limits Clicked", event = "Limits Clicked",
) )
data object NoticeBalancesInfo : MainScreenAnalyticsEvent( class NoticeBalancesInfo : MainScreenAnalyticsEvent(
event = "Notice - Balances Info", event = "Notice - Balances Info",
) )
data object NoticeLimitsInfo : MainScreenAnalyticsEvent( class NoticeLimitsInfo : MainScreenAnalyticsEvent(
event = "Notice - Limits Info", event = "Notice - Limits Info",
) )
data object ButtonExplore : MainScreenAnalyticsEvent( class ButtonExplore : MainScreenAnalyticsEvent(
event = "Button - Explore", event = "Button - Explore",
) )
class AccountShowTokens : MainScreenAnalyticsEvent(
event = "Button - Account Show Tokens",
)
class AccountHideTokens : MainScreenAnalyticsEvent(
event = "Button - Account Hide Tokens",
)
data class ButtonSwap(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent( data class ButtonSwap(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent(
event = "Button - Swap", event = "Button - Swap",
params = mapOf(AnalyticsParam.STATUS to status.value), params = mapOf(AnalyticsParam.STATUS to status.value),
@ -66,11 +74,11 @@ sealed class MainScreenAnalyticsEvent(
params = mapOf(AnalyticsParam.STATUS to status.value), params = mapOf(AnalyticsParam.STATUS to status.value),
) )
data object BuyScreenOpened : MainScreenAnalyticsEvent(event = "Buy Screen Opened") class BuyScreenOpened : MainScreenAnalyticsEvent(event = "Buy Screen Opened")
data object SwapScreenOpened : MainScreenAnalyticsEvent(event = "Swap Screen Opened") class SwapScreenOpened : MainScreenAnalyticsEvent(event = "Swap Screen Opened")
data object SellScreenOpened : MainScreenAnalyticsEvent(event = "Sell Screen Opened") class SellScreenOpened : MainScreenAnalyticsEvent(event = "Sell Screen Opened")
data class BuyTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent( data class BuyTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent(
event = "Buy Token Clicked", event = "Buy Token Clicked",

View file

@ -12,11 +12,92 @@ sealed class OnboardingAnalyticsEvent(
sealed class Onboarding( sealed class Onboarding(
event: String, event: String,
params: Map<String, String> = mapOf(), params: Map<String, String> = mapOf(),
) : OnboardingAnalyticsEvent(category = "Onboarding", event = event, params = params) {
class Started(
source: String,
) : Onboarding(
event = "Onboarding Started",
params = mapOf(
AnalyticsParam.SOURCE to source,
),
)
class Finished(
source: String,
) : Onboarding(
event = "Onboarding Finished",
params = mapOf(
AnalyticsParam.SOURCE to source,
),
)
class ButtonMobileWallet(
source: String,
) : Onboarding(
event = "Button - Mobile Wallet",
params = mapOf(
AnalyticsParam.SOURCE to source,
),
)
}
sealed class CreateWallet(
event: String,
params: Map<String, String> = mapOf(),
) : OnboardingAnalyticsEvent(category = "Onboarding / Create Wallet", event = event, params = params) {
class ButtonCreateWallet : CreateWallet("Button - Create Wallet")
class WalletCreatedSuccessfully(
source: String,
creationType: WalletCreationType = WalletCreationType.NewSeed,
seedPhraseLength: Int? = null,
passPhraseState: AnalyticsParam.EmptyFull,
) : CreateWallet(
event = "Wallet Created Successfully",
params = buildMap {
put(AnalyticsParam.SOURCE, source)
put("Creation Type", creationType.value)
put("Passphrase", passPhraseState.value)
if (seedPhraseLength != null) {
put("Seed Phrase Length", seedPhraseLength.toString())
}
},
)
sealed class WalletCreationType(val value: String) {
data object NewSeed : WalletCreationType(value = "New Seed")
data object SeedImport : WalletCreationType(value = "Seed Import")
}
}
sealed class SeedPhrase(
event: String,
params: Map<String, String> = mapOf(),
) : OnboardingAnalyticsEvent(category = "Onboarding / Seed Phrase", event = event, params = params) {
class CreateMobileScreenOpened(
source: String,
) : SeedPhrase(
event = "Create Mobile Screen Opened",
params = mapOf(
AnalyticsParam.SOURCE to source,
),
)
class ButtonImportWallet : SeedPhrase("Button - Import Wallet")
class ImportSeedPhraseScreenOpened : SeedPhrase("Import Seed Phrase Screen Opened")
class ButtonImport : SeedPhrase("Button - Import")
}
sealed class Error(
event: String,
params: Map<String, String> = mapOf(),
) : OnboardingAnalyticsEvent(category = "Error", event = event, params = params) { ) : OnboardingAnalyticsEvent(category = "Error", event = event, params = params) {
data class OfflineAttestationFailed( data class OfflineAttestationFailed(
val source: AnalyticsParam.ScreensSources, val source: AnalyticsParam.ScreensSources,
) : Onboarding( ) : Error(
event = "Offline Attestation Failed", event = "Offline Attestation Failed",
params = mapOf(AnalyticsParam.SOURCE to source.value), params = mapOf(AnalyticsParam.SOURCE to source.value),
) )

View file

@ -0,0 +1,52 @@
package com.tangem.core.analytics.models.event
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
sealed class SignIn(
event: String,
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent("Sign In", event, params) {
data class ScreenOpened(
val walletsCount: Int,
) : SignIn(
event = "Sign In Screen Opened",
params = mapOf(
"Wallets Count" to walletsCount.toString(),
),
)
class ButtonBiometricSignIn : SignIn(event = "Button - Biometric Sign In")
class ButtonUnlockAllWithBiometric : SignIn(event = "Button - Unlock All With Biometric")
class ErrorBiometricUpdated : SignIn(event = "Error - Biometric Updated")
class ButtonWallet(
signInType: SignInType,
walletsCount: Int,
) : SignIn(
event = "Button - Wallet",
params = buildMap {
put("Wallets Count", walletsCount.toString())
put("Sign in type", signInType.value)
},
) {
enum class SignInType(val value: String) {
Card("Card"),
Biometric("Biometric"),
NoSecurity("No Security"),
AccessCode("Access Code"),
}
}
data class ButtonAddWallet(
val sources: AnalyticsParam.ScreensSources,
) : SignIn(
event = "Button - Add Wallet",
params = mapOf(
AnalyticsParam.SOURCE to sources.value,
),
)
}

View file

@ -33,7 +33,7 @@
}, },
{ {
"name": "HOT_WALLET_ENABLED", "name": "HOT_WALLET_ENABLED",
"version": "undefined" "version": "5.32.0"
}, },
{ {
"name": "TANGEM_PAY_ENABLED", "name": "TANGEM_PAY_ENABLED",

View file

@ -22,6 +22,9 @@ enum class ApiEnvironment {
@Json(name = "STAGE") @Json(name = "STAGE")
STAGE, STAGE,
@Json(name = "STAGE_2")
STAGE_2,
@Json(name = "MOCK") @Json(name = "MOCK")
MOCK, MOCK,

View file

@ -30,6 +30,7 @@ internal class Express(
createDev2Environment(), createDev2Environment(),
createDev3Environment(), createDev3Environment(),
createStageEnvironment(), createStageEnvironment(),
createStage2Environment(),
createMockedEnvironment(), createMockedEnvironment(),
createProdEnvironment(), createProdEnvironment(),
) )
@ -73,6 +74,12 @@ internal class Express(
headers = createHeaders(isProd = false), headers = createHeaders(isProd = false),
) )
private fun createStage2Environment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.STAGE_2,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(isProd = false),
)
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK, environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]", baseUrl = "[REDACTED_ENV_URL]",

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.api.common.config package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig import com.tangem.datasource.BuildConfig
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
import com.tangem.lib.auth.P2PEthPoolAuthProvider import com.tangem.lib.auth.P2PEthPoolAuthProvider
import com.tangem.utils.ProviderSuspend import com.tangem.utils.ProviderSuspend
@ -22,12 +23,7 @@ internal class P2PEthPool(
private fun getInitialEnvironment(): ApiEnvironment { private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) { return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
DEBUG_BUILD_TYPE, else -> if (P2PStakingConfig.USE_TESTNET) ApiEnvironment.DEV else ApiEnvironment.PROD
INTERNAL_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
} }
} }

View file

@ -77,6 +77,7 @@ internal class YieldSupply(
ApiEnvironment.DEV_2, ApiEnvironment.DEV_2,
ApiEnvironment.DEV_3, ApiEnvironment.DEV_3,
ApiEnvironment.STAGE, ApiEnvironment.STAGE,
ApiEnvironment.STAGE_2,
-> environmentConfigStorage.getConfigSync().yieldModuleApiKeyDev -> environmentConfigStorage.getConfigSync().yieldModuleApiKeyDev
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().yieldModuleApiKey ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().yieldModuleApiKey
} ?: error("No tangem tech api config provided") } ?: error("No tangem tech api config provided")

View file

@ -23,9 +23,7 @@ interface P2PEthPoolApi {
* @param network Ethereum pool network: "mainnet" or "hoodi" (testnet) * @param network Ethereum pool network: "mainnet" or "hoodi" (testnet)
*/ */
@GET("api/v1/staking/pool/{network}/vaults") @GET("api/v1/staking/pool/{network}/vaults")
suspend fun getVaults( suspend fun getVaults(@Path("network") network: String): ApiResponse<P2PEthPoolResponse<P2PEthPoolVaultsResponse>>
@Path("network") network: String = "mainnet",
): ApiResponse<P2PEthPoolResponse<P2PEthPoolVaultsResponse>>
/** /**
* Prepare deposit transaction * Prepare deposit transaction

View file

@ -38,6 +38,6 @@ interface NewsApi {
private companion object { private companion object {
private const val NEWS_PATH = "api/v1/news" private const val NEWS_PATH = "v1/news"
} }
} }

View file

@ -5,11 +5,5 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class NewsTrendingResponse( data class NewsTrendingResponse(
@Json(name = "meta") val meta: NewsTrendingMetaDto,
@Json(name = "items") val items: List<NewsArticleDto>, @Json(name = "items") val items: List<NewsArticleDto>,
)
@JsonClass(generateAdapter = true)
data class NewsTrendingMetaDto(
@Json(name = "limit") val limit: Int,
) )

View file

@ -50,6 +50,12 @@ interface TangemTechApi {
@Body userTokens: UserTokensResponse, @Body userTokens: UserTokensResponse,
): ApiResponse<Unit> ): ApiResponse<Unit>
@PUT("/v1/wallets/{walletId}/tokens")
suspend fun saveTokens(
@Path(value = "walletId") userId: String,
@Body userTokens: UserTokensResponse,
): ApiResponse<Unit>
/** Returns referral status by [walletId] */ /** Returns referral status by [walletId] */
@GET("v1/referral/{walletId}") @GET("v1/referral/{walletId}")
suspend fun getReferralStatus(@Path("walletId") walletId: String): ApiResponse<ReferralResponse> suspend fun getReferralStatus(@Path("walletId") walletId: String): ApiResponse<ReferralResponse>
@ -129,6 +135,12 @@ interface TangemTechApi {
@Body body: List<WalletIdBody>, @Body body: List<WalletIdBody>,
): ApiResponse<Unit> ): ApiResponse<Unit>
@PUT("/v1/user-wallets/applications/{application_id}/wallets")
suspend fun associateApplicationIdWithWalletsV2(
@Path("application_id") applicationId: String,
@Body body: AssociateApplicationIdWithWalletsBody,
): ApiResponse<Unit>
@GET("v1/user-wallets/wallets/{wallet_id}") @GET("v1/user-wallets/wallets/{wallet_id}")
suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse<WalletResponse> suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse<WalletResponse>

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class AssociateAppWithWalletsErrorResponse(
@Json(name = "missingWalletIds") val missingWalletIds: List<String>,
)

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class AssociateApplicationIdWithWalletsBody(
@Json(name = "walletIds") val walletIds: List<String>,
)

View file

@ -19,6 +19,7 @@ data class GetWalletAccountsResponse(
@Json(name = "group") val group: GroupType?, @Json(name = "group") val group: GroupType?,
@Json(name = "sort") val sort: SortType?, @Json(name = "sort") val sort: SortType?,
@Json(name = "totalAccounts") val totalAccounts: Int, @Json(name = "totalAccounts") val totalAccounts: Int,
@Json(name = "totalArchivedAccounts") val totalArchivedAccounts: Int,
) )
} }

View file

@ -15,4 +15,7 @@ interface AppCurrencyResponseStore {
/** Get [CurrenciesResponse.Currency] synchronously or null */ /** Get [CurrenciesResponse.Currency] synchronously or null */
suspend fun getSyncOrNull(): CurrenciesResponse.Currency? suspend fun getSyncOrNull(): CurrenciesResponse.Currency?
/** Store [CurrenciesResponse.Currency] */
suspend fun store(currency: CurrenciesResponse.Currency)
} }

View file

@ -5,6 +5,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject import com.tangem.datasource.local.preferences.utils.getObject
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
/** /**
@ -25,4 +26,11 @@ internal class DefaultAppCurrencyResponseStore(
PreferencesKeys.SELECTED_APP_CURRENCY_KEY, PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
) )
} }
override suspend fun store(currency: CurrenciesResponse.Currency) {
appPreferencesStore.storeObject(
PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
currency,
)
}
} }

View file

@ -1,7 +1,10 @@
package com.tangem.datasource.di package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.promo.DefaultPromoBannerStore
import com.tangem.datasource.local.promo.DefaultPromoStoriesStore import com.tangem.datasource.local.promo.DefaultPromoStoriesStore
import com.tangem.datasource.local.promo.PromoBannerStore
import com.tangem.datasource.local.promo.PromoStoriesStore import com.tangem.datasource.local.promo.PromoStoriesStore
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -18,4 +21,10 @@ object PromoStoreModule {
fun providePromoStoriesStore(): PromoStoriesStore { fun providePromoStoriesStore(): PromoStoriesStore {
return DefaultPromoStoriesStore(dataStore = RuntimeDataStore()) return DefaultPromoStoriesStore(dataStore = RuntimeDataStore())
} }
@Provides
@Singleton
fun providePromoBannerStore(): PromoBannerStore {
return DefaultPromoBannerStore(dataStore = RuntimeSharedStore())
}
} }

View file

@ -5,6 +5,7 @@ import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
import com.tangem.datasource.local.datastore.RuntimeDataStore import com.tangem.datasource.local.datastore.RuntimeDataStore
@ -77,6 +78,24 @@ internal object StakingStoreModule {
return DefaultStakingActionsStore(dataStore = RuntimeDataStore()) return DefaultStakingActionsStore(dataStore = RuntimeDataStore())
} }
@Provides
@Singleton
fun provideP2PBalancesPersistenceStore(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): DataStore<Map<String, Set<P2PEthPoolAccountResponse>>> {
return DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = mapWithStringKeyTypes(valueTypes = setTypes<P2PEthPoolAccountResponse>()),
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile(fileName = "p2p_balances") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
)
}
@Provides @Provides
@Singleton @Singleton
fun provideP2PEthPoolVaultsStore( fun provideP2PEthPoolVaultsStore(

View file

@ -0,0 +1,19 @@
package com.tangem.datasource.local.promo
import com.tangem.datasource.api.promotion.models.PromoBannerResponse
import com.tangem.datasource.local.datastore.RuntimeSharedStore
internal class DefaultPromoBannerStore(
private val dataStore: RuntimeSharedStore<Map<String, PromoBannerResponse>>,
) : PromoBannerStore {
override suspend fun getSyncOrNull(promoId: String): PromoBannerResponse? {
return dataStore.getSyncOrNull()?.get(promoId)
}
override suspend fun store(promoId: String, promoBanner: PromoBannerResponse) {
dataStore.update(emptyMap()) { current ->
current + (promoId to promoBanner)
}
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.local.promo
import com.tangem.datasource.api.promotion.models.PromoBannerResponse
interface PromoBannerStore {
suspend fun getSyncOrNull(promoId: String): PromoBannerResponse?
suspend fun store(promoId: String, promoBanner: PromoBannerResponse)
}

View file

@ -4,28 +4,32 @@ import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.domain.models.StatusSource import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.staking.BalanceItem import com.tangem.domain.models.staking.BalanceItem
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.staking.YieldBalanceItem import com.tangem.domain.models.staking.YieldBalanceItem
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
class YieldBalanceConverter( /**
* Converts StakeKit DTO to [StakingBalance].
* Returns [StakingBalance.Data.StakeKit] for non-empty balances, [StakingBalance.Empty] otherwise.
*/
class StakingBalanceConverter(
private val source: StatusSource, private val source: StatusSource,
) : Converter<YieldBalanceWrapperDTO, YieldBalance?> { ) : Converter<YieldBalanceWrapperDTO, StakingBalance?> {
constructor(isCached: Boolean) : this(source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL) constructor(isCached: Boolean) : this(source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL)
override fun convert(value: YieldBalanceWrapperDTO): YieldBalance? { override fun convert(value: YieldBalanceWrapperDTO): StakingBalance? {
val stakingId = StakingID( val stakingId = StakingID(
integrationId = value.integrationId ?: return null, integrationId = value.integrationId ?: return null,
address = value.addresses.address, address = value.addresses.address,
) )
return if (value.balances.isEmpty()) { return if (value.balances.isEmpty()) {
YieldBalance.Empty(stakingId = stakingId, source = source) StakingBalance.Empty(stakingId = stakingId, source = source)
} else { } else {
YieldBalance.Data( StakingBalance.Data.StakeKit(
stakingId = stakingId, stakingId = stakingId,
balance = YieldBalanceItem( balance = YieldBalanceItem(
items = value.balances items = value.balances

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