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.yieldSupply)
implementation(projects.data.hotWallet)
implementation(projects.data.news)
/** Features */
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.runner.Description
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
get() = ApplicationProvider.getApplicationContext()
private var originalFeatureTogglesValues: Map<String, String>? = null
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
saveOriginalFeatureToggles()
overrideFeatureToggles()
OnComponentReadyRunner.addListener(
tangemApplication, ApplicationEntryPoint::class.java
) { _: ApplicationEntryPoint ->
tangemApplication.preInit()
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_VALUE
import com.tangem.common.rules.ApiEnvironmentRule
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
@ -53,9 +51,6 @@ abstract class BaseTestCase : TestCase(
@Inject
lateinit var appPreferencesStore: AppPreferencesStore
@Inject
lateinit var featureTogglesManager: FeatureTogglesManager
@Inject
lateinit var promoRepository: PromoRepository
@ -75,7 +70,7 @@ abstract class BaseTestCase : TestCase(
@JvmField
val ruleChain: TestRule = RuleChain
.outerRule(hiltRule)
.around(ApplicationInjectionExecutionRule())
.around(applicationInjectionRule())
.around(permissionRule)
.around(apiEnvironmentRule)
.around(composeTestRule)
@ -110,7 +105,6 @@ abstract class BaseTestCase : TestCase(
apiEnvironmentRule.setup(apiConfigsManager)
ActivityScenario.launch(MainActivity::class.java)
Intents.init()
setFeatureToggles()
additionalBeforeSection()
}.after {
additionalAfterSection()
@ -141,14 +135,15 @@ abstract class BaseTestCase : TestCase(
fun waitForIdle() = composeTestRule.waitForIdle()
private fun setFeatureToggles() {
runBlocking {
with(featureTogglesManager as MutableFeatureTogglesManager) {
changeToggle("NEW_TOKEN_RECEIVE_ENABLED", true)
changeToggle("WALLET_BALANCE_FETCHER_ENABLED", true)
changeToggle("SWAP_REDESIGN_ENABLED", true)
changeToggle("NEW_ONRAMP_MAIN_ENABLED", true)
}
}
private fun applicationInjectionRule(): ApplicationInjectionExecutionRule {
return ApplicationInjectionExecutionRule(
toggleStates = mapOf(
"NEW_TOKEN_RECEIVE_ENABLED" to true,
"WALLET_BALANCE_FETCHER_ENABLED" to true,
"SWAP_REDESIGN_ENABLED" to 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") {
onDisclaimerScreen { acceptButton.clickWithAssertion() }
}
step("Click on 'Scan' button") {
onStoriesScreen { scanButton.clickWithAssertion() }
step("Click on 'Get started' button") {
onStoriesScreen { getStartedButton.clickWithAssertion() }
}
step("Click on 'Scan card or ring' button") {
onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() }
}
if (alreadyActivatedDialogIsShown) {
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)
hasText(getResourceString(R.string.app_settings_title))
}
val contactSupportButton: KNode = child {
hasTestTag(DetailsScreenTestTags.SCREEN_ITEM)
hasText(getResourceString(R.string.common_contact_support))
@ -50,6 +51,11 @@ class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasTestTag(DetailsScreenTestTags.SCREEN_ITEM)
hasText(getResourceString(R.string.disclaimer_title))
}
val versionName: KNode = child {
hasTestTag(DetailsScreenTestTags.VERSION_NAME)
useUnmergedTree = true
}
}
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.kakao.common.utilities.getResourceString
import com.tangem.features.send.v2.impl.R as SendR
import androidx.compose.ui.test.hasText as withText
class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendPageObject>(semanticsProvider = semanticsProvider) {
@ -38,6 +39,11 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
useUnmergedTree = true
}
val amountErrorText: KNode = child {
hasTestTag(SendScreenTestTags.AMOUNT_ERROR_TEXT)
useUnmergedTree = true
}
val equivalentInputAmount: KNode = child {
hasTestTag(SendScreenTestTags.EQUIVALENT_INPUT_AMOUNT)
useUnmergedTree = true
@ -69,8 +75,8 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
}
val nextButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(SendR.string.common_next))
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyDescendant(withText(getResourceString(SendR.string.common_next)))
useUnmergedTree = true
}

View file

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

View file

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

View file

@ -139,8 +139,11 @@ class FeedbackTest : BaseTestCase() {
step("Set scanning error") {
MockProvider.setEmulateError(TangemSdkError.TagLost())
}
step("Click on 'Scan' button") {
onStoriesScreen { scanButton.performClick() }
step("Click on 'Get started' button") {
onStoriesScreen { getStartedButton.clickWithAssertion() }
}
step("Click on 'Scan card or ring' button") {
onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() }
}
step("Force show 'Scan warning' dialog"){
runOnUiThread {
@ -178,8 +181,11 @@ class FeedbackTest : BaseTestCase() {
step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() }
}
step("Click on 'Scan' button") {
onStoriesScreen { scanButton.clickWithAssertion() }
step("Click on 'Get started' button") {
onStoriesScreen { getStartedButton.clickWithAssertion() }
}
step("Click on 'Scan card or ring' button") {
onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() }
}
step("Check 'Already used Wallet' dialog") {
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") {
onStoriesScreen {
scanButton.assertIsDisplayed()
orderButton.assertIsDisplayed()
getStartedButton.assertIsDisplayed()
}
}
}
@ -91,8 +90,7 @@ class TermsOfServiceTest : BaseTestCase() {
}
step("Assert 'Stories' screen is opened") {
onStoriesScreen {
scanButton.assertIsDisplayed()
orderButton.assertIsDisplayed()
getStartedButton.assertIsDisplayed()
}
}
step("Stop app") {
@ -103,8 +101,7 @@ class TermsOfServiceTest : BaseTestCase() {
}
step("Assert 'Stories' screen is opened") {
onStoriesScreen {
scanButton.assertIsDisplayed()
orderButton.assertIsDisplayed()
getStartedButton.assertIsDisplayed()
}
}
}

View file

@ -276,7 +276,10 @@ class RecentBlockTest : BaseTestCase() {
}
step("Swipe up") {
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") {
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 amountToLeaveLessThanRent = "0.0016941"
private val amountToLeaveGreaterThanRent = "0.0000941"
private val amountToLeaveRentOnly = "0.00168934"
private val amountToLeaveRentOnly = "0.001689338"
private val rentAmount = "SOL 0.00089088"
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.nfc"
android:required="true" />
android:required="false" />
<queries>
<intent>

View file

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

View file

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

View file

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

View file

@ -12,12 +12,6 @@ sealed class AnalyticsParam {
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) {
data object Liked : RateApp("Liked")
data object Closed : RateApp("Close")

View file

@ -11,85 +11,5 @@ sealed class Onboarding(
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category, event, params) {
class Started : Onboarding("Onboarding", "Onboarding Started")
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(),
) {
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),
)
object ButtonEnableBiometricAuthentication : AppSettings(event = "Button - Enable Biometric Authentication")
class ButtonEnableBiometricAuthentication : AppSettings(event = "Button - Enable Biometric Authentication")
class MainCurrencyChanged(currencyType: String) : AppSettings(
event = "Main Currency Changed",
@ -79,7 +79,7 @@ sealed class Settings(
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(
event = "Hide Balance Changed",

View file

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

View file

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

View file

@ -49,7 +49,7 @@ class FirebaseAnalyticsHandler(
class Builder : AnalyticsHandlerBuilder {
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = when {
!data.isDebug -> FirebaseClient()
data.isDebug && data.logConfig.firebase -> FirebaseLogClient(data.jsonConverter)
data.isDebug && data.logConfig.isFirebaseLogEnabled -> FirebaseLogClient(data.jsonConverter)
else -> null
}?.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.models.AnalyticsEvent
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.ParamCardCurrencyConverter
import com.tangem.domain.card.common.util.cardTypesResolver
@ -30,7 +31,11 @@ class CardContextInterceptor(
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean {
return when (event) {
is IntroductionProcess.ButtonScanCard -> false
is IntroductionProcess.ButtonScanCard,
is IntroductionProcess.ButtonScanCardLegacy,
is SignIn.ScreenOpened,
is SignIn.ButtonAddWallet,
-> false
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.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.SignIn
import com.tangem.domain.card.analytics.IntroductionProcess
class HotWalletContextInterceptor(
val parent: ParamsInterceptor? = null,
@ -10,10 +12,22 @@ class HotWalletContextInterceptor(
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>) {
params[AnalyticsParam.PRODUCT_TYPE] = AnalyticsParam.ProductType.MobileWallet.value
params.remove(AnalyticsParam.BATCH)
params.remove(AnalyticsParam.FIRMWARE)
params.remove(AnalyticsParam.CURRENCY)
}
companion object {

View file

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

View file

@ -1,6 +1,8 @@
package com.tangem.tap.di.domain
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.repository.HotWalletRepository
import dagger.Module
@ -24,4 +26,18 @@ internal object HotWalletDomainModule {
fun provideSetAccessCodeSkippedUseCase(hotWalletRepository: HotWalletRepository): SetAccessCodeSkippedUseCase {
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.quotes.multi.MultiQuoteStatusFetcher
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.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
@ -74,7 +74,7 @@ internal object ManageTokensDomainModule {
derivationsRepository: DerivationsRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
): SaveManagedTokensUseCase {
@ -85,7 +85,7 @@ internal object ManageTokensDomainModule {
derivationsRepository = derivationsRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
stakingIdFactory = stakingIdFactory,
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.settings.repositories.SettingsRepository
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.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.hotwallet.HotWalletFeatureToggles
@ -65,20 +66,22 @@ object MarketsDomainModule {
fun provideSaveMarketTokensUseCase(
derivationsRepository: DerivationsRepository,
marketsTokenRepository: MarketsTokenRepository,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
): SaveMarketTokensUseCase {
return SaveMarketTokensUseCase(
derivationsRepository = derivationsRepository,
marketsTokenRepository = marketsTokenRepository,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
stakingIdFactory = stakingIdFactory,
parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default),
)

View file

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

View file

@ -1,10 +1,7 @@
package com.tangem.tap.di.domain
import com.tangem.domain.news.repository.NewsRepository
import com.tangem.domain.news.usecase.GetNewsCategoriesUseCase
import com.tangem.domain.news.usecase.GetNewsListBatchFlowUseCase
import com.tangem.domain.news.usecase.ObserveNewsDetailsUseCase
import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase
import com.tangem.domain.news.usecase.*
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -38,4 +35,10 @@ internal object NewsDomainModule {
fun provideGetNewsListBatchFlowUseCase(repository: NewsRepository): GetNewsListBatchFlowUseCase {
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.repositories.*
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import dagger.Module
import dagger.Provides
@ -264,12 +265,14 @@ internal object OnrampDomainModule {
onrampErrorResolver: OnrampErrorResolver,
onrampTransactionRepository: OnrampTransactionRepository,
settingsRepository: SettingsRepository,
promoRepository: PromoRepository,
): GetOnrampOffersUseCase {
return GetOnrampOffersUseCase(
onrampRepository = onrampRepository,
errorResolver = onrampErrorResolver,
onrampTransactionRepository = onrampTransactionRepository,
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.StakingRepository
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.single.SingleStakingBalanceFetcher
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import dagger.Module
@ -113,11 +113,11 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideFetchStakingYieldBalanceUseCase(
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
singleStakingBalanceFetcher: SingleStakingBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): FetchStakingYieldBalanceUseCase {
return FetchStakingYieldBalanceUseCase(
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
singleStakingBalanceFetcher = singleStakingBalanceFetcher,
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.single.SingleQuoteStatusSupplier
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
import com.tangem.domain.staking.single.SingleStakingBalanceSupplier
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
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.walletmanager.WalletManagersFacade
import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles
@ -40,17 +43,19 @@ internal object TokensDomainModule {
@Singleton
fun provideAddCryptoCurrenciesUseCase(
currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
singleStakingBalanceFetcher: SingleStakingBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory,
): AddCryptoCurrenciesUseCase {
return AddCryptoCurrenciesUseCase(
currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
singleStakingBalanceFetcher = singleStakingBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
stakingIdFactory = stakingIdFactory,
)
@ -148,7 +153,7 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
singleStakingBalanceFetcher: SingleStakingBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory,
): FetchCurrencyStatusUseCase {
@ -156,7 +161,7 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
singleStakingBalanceFetcher = singleStakingBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
stakingIdFactory = stakingIdFactory,
)
@ -339,8 +344,8 @@ internal object TokensDomainModule {
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceSupplier: MultiYieldBalanceSupplier,
singleStakingBalanceSupplier: SingleStakingBalanceSupplier,
multiStakingBalanceSupplier: MultiStakingBalanceSupplier,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory,
): BaseCurrencyStatusOperations {
@ -350,8 +355,8 @@ internal object TokensDomainModule {
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceSupplier = multiYieldBalanceSupplier,
singleStakingBalanceSupplier = singleStakingBalanceSupplier,
multiStakingBalanceSupplier = multiStakingBalanceSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
stakingIdFactory = stakingIdFactory,
)
@ -371,7 +376,7 @@ internal object TokensDomainModule {
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
): WalletBalanceFetcher {
@ -381,7 +386,7 @@ internal object TokensDomainModule {
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
stakingIdFactory = stakingIdFactory,
dispatchers = dispatchers,
)

View file

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

View file

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

View file

@ -7,12 +7,16 @@ import arrow.core.raise.either
import arrow.core.right
import com.tangem.common.*
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.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository.LockMethod
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.UserWalletId
import com.tangem.domain.models.wallet.isLocked
@ -50,6 +54,9 @@ internal class DefaultUserWalletsListRepository(
private val appPreferencesStore: AppPreferencesStore,
private val hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
private val tangemHotSdk: TangemHotSdk,
private val trackingContextProxy: TrackingContextProxy,
private val analyticsEventHandler: AnalyticsEventHandler,
private val hotWalletRepository: HotWalletRepository,
) : UserWalletsListRepository {
override val userWallets = MutableStateFlow<List<UserWallet>?>(null)
@ -204,7 +211,7 @@ internal class DefaultUserWalletsListRepository(
userWalletEncryptionKeysRepository.delete(userWalletIds)
removeHotWalletsFromSDK(userWalletIds)
removeHotWalletsFromSDKAndRepos(userWalletIds)
userWallets.update { currentWallets ->
val updatedWallets = currentWallets?.filter { userWalletIds.contains(it.walletId).not() }
@ -235,6 +242,7 @@ internal class DefaultUserWalletsListRepository(
when (unlockMethod) {
UserWalletsListRepository.UnlockMethod.Biometric -> {
unlockAllWallets().bind()
trackSignInEvent(userWallet, Basic.SignedIn.SignInType.Biometric)
select(userWalletId)
}
UserWalletsListRepository.UnlockMethod.AccessCode -> {
@ -263,7 +271,10 @@ internal class DefaultUserWalletsListRepository(
removePasswordAttempts(userWallet)
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)) }
}
is UserWalletsListRepository.UnlockMethod.Scan -> {
@ -291,7 +302,10 @@ internal class DefaultUserWalletsListRepository(
)
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)) }
}
}
@ -332,6 +346,9 @@ internal class DefaultUserWalletsListRepository(
sensitiveInformationRepository.getAll(allKeys)
.doOnSuccess { sensitiveInfo ->
updateWallets { wallets -> wallets?.updateWith(sensitiveInfo) }
selectedUserWallet.value?.let {
trackSignInEvent(it, Basic.SignedIn.SignInType.Biometric)
}
}
.doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) }
}
@ -373,7 +390,7 @@ internal class DefaultUserWalletsListRepository(
if (newUserWallet.walletId == oldUserWallet.walletId &&
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
if (hasBiometry()) {
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()
.filterIsInstance<UserWallet.Hot>()
.filter { walletIds.contains(it.walletId) }
hotWalletsToDelete.forEach {
tangemHotSdk.delete(it.hotWalletId)
hotWalletsToDelete.forEach { wallet ->
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()
}
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.Types
import com.tangem.common.authentication.storage.AuthenticatedStorage
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.secure.SecureStorage
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.hot.sdk.android.crypto.AESEncryptionProtocol
@ -96,7 +97,13 @@ internal class UserWalletEncryptionKeysRepository(
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()
}
}

View file

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

View file

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

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
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.res.TangemTheme
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.wallet.R
private const val CARD_PLACEHOLDER_SECONDARY_ROTATION = -15f
private const val CARD_PLACEHOLDER_PRIMARY_ROTATION = -1f
@Composable
internal fun CardSettingsScreen(state: CardSettingsScreenState, modifier: Modifier = Modifier) {
val isCardReadingNeeded = state.cardDetails == null
@ -42,74 +46,82 @@ internal fun CardSettingsScreen(state: CardSettingsScreenState, modifier: Modifi
)
}
@Suppress("MagicNumber")
@Composable
private fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Box(
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,
)
}
CardPlaceholderImages()
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
.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,
modifier = Modifier
.verticalScroll(rememberScrollState())
.weight(weight = 1f, fill = false),
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size32))
DetailsMainButton(
title = stringResourceSafe(id = R.string.scan_card_settings_button),
onClick = onScanCardClick,
)
}
start = TangemTheme.dimens.spacing80,
end = TangemTheme.dimens.spacing80,
top = TangemTheme.dimens.spacing70,
)
.rotate(CARD_PLACEHOLDER_SECONDARY_ROTATION),
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(CARD_PLACEHOLDER_PRIMARY_ROTATION),
painter = painterResource(id = R.drawable.card_placeholder_black),
contentDescription = null,
contentScale = ContentScale.FillWidth,
)
}
}
@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
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.ui.securitymode.toTitleRes
import com.tangem.wallet.R
@ -32,7 +30,7 @@ internal sealed class CardInfo(
class SignedHashes(hashes: String) : CardInfo(
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(
@ -47,7 +45,7 @@ internal sealed class CardInfo(
isClickable = true,
)
class AccessCodeRecovery(val isEnabled: Boolean) : CardInfo(
class AccessCodeRecovery(isEnabled: Boolean) : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_access_code_recovery_title),
subtitle = if (isEnabled) {
TextReference.Res(R.string.common_enabled)
@ -62,22 +60,4 @@ internal sealed class CardInfo(
subtitle = description,
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
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
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.TangemThemePreview
import com.tangem.core.ui.test.ResetCardScreenTestTags
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@ -198,7 +198,7 @@ private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) {
}
@Composable
private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) {
private fun CommonResetDialog(dialog: ResetCardDialog) {
BasicDialog(
title = stringResourceSafe(dialog.titleResId),
message = stringResourceSafe(dialog.messageResId),

View file

@ -1,7 +1,7 @@
package com.tangem.tap.features.details.ui.resetcard
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
internal data class ResetCardScreenState(

View file

@ -18,12 +18,12 @@ object UnfinishedBackupFoundDialog {
setTitle(R.string.common_warning)
setMessage(R.string.welcome_interrupted_backup_alert_message)
setPositiveButton(R.string.welcome_interrupted_backup_alert_resume) { _, _ ->
Analytics.send(OnboardingEvent.Backup.ResumeInterruptedBackup)
Analytics.send(OnboardingEvent.Backup.ResumeInterruptedBackup())
store.dispatch(GlobalAction.HideDialog)
store.dispatch(BackupAction.ResumeFoundUnfinishedBackup(scanResponse))
}
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.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.tap.common.analytics.events.SignIn
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.redux.WelcomeAction
import com.tangem.tap.features.welcome.redux.WelcomeState

View file

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

View file

@ -1,6 +1,6 @@
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
internal data class WelcomeScreenState(

View file

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

View file

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

View file

@ -1,6 +1,7 @@
package com.tangem.tap.network.auth
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
import com.tangem.lib.auth.P2PEthPoolAuthProvider
internal class DefaultP2PEthPoolAuthProvider(
@ -11,6 +12,6 @@ internal class DefaultP2PEthPoolAuthProvider(
val keys = environmentConfigStorage.getConfigSync().p2pApiKey
?: 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.tangem.common.routing.AppRoute
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.models.Basic
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.child
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.onboarding.repository.OnboardingRepository
import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy
import com.tangem.features.walletconnect.components.WcRoutingComponent
import com.tangem.hot.sdk.TangemHotSdk
@ -63,6 +67,9 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
private val userWalletsListRepository: UserWalletsListRepository,
private val cardRepository: CardRepository,
private val onboardingRepository: OnboardingRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val trackingContextProxy: TrackingContextProxy,
private val analyticsEventHandler: AnalyticsEventHandler,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
) : RoutingComponent,
AppComponentContext by context,
@ -151,6 +158,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
)
}
else -> {
trackSignInEvent()
AppRoute.Wallet
}
}.also {
@ -235,4 +243,18 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
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")
fun create(appRoute: AppRoute): StackAnimator {
return when (appRoute) {
is AppRoute.Onboarding,
is AppRoute.Welcome,
is AppRoute.Home,
-> fade(tween(400)).plus(scale(tween(400)))

View file

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