diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b5b03c3345..bb2b8ab488 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt b/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt index 75662e25d4..2445305b72 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt @@ -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 +) : TestRule { private val tangemApplication: TangemApplication get() = ApplicationProvider.getApplicationContext() + private var originalFeatureTogglesValues: Map? = 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 + } 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) + + 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}") + } + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 57a2c29563..8a40e56185 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -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 + ) + ) } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt index 1761e307c2..e84ee3a24c 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt @@ -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") { diff --git a/app/src/androidTest/kotlin/com/tangem/screens/CreateWalletStartPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/CreateWalletStartPageObject.kt new file mode 100644 index 0000000000..b06335d752 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/CreateWalletStartPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt index 863e387a71..4c172531a0 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt @@ -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) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SendPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SendPageObject.kt index 1e1f1da9d2..6cc240110b 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SendPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SendPageObject.kt @@ -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(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 } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StoriesPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/StoriesPageObject.kt index f9666ceeb4..faf2492a94 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/StoriesPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/StoriesPageObject.kt @@ -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(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 } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt index 49fe556eba..c084d381c8 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt @@ -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() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt index 9d75140152..0a20f32754 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt @@ -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() diff --git a/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt deleted file mode 100644 index c31340f9dd..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt +++ /dev/null @@ -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() - } - } - } -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt deleted file mode 100644 index d87e1be98c..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt +++ /dev/null @@ -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() - } - } - } -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt index 3b956d2bea..046964ff53 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt @@ -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() } } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt index 1a169ead0b..d7f75da532 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt @@ -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) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/amountScreen/SendAmountScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/amountScreen/SendAmountScreenTest.kt new file mode 100644 index 0000000000..dd6f386748 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/amountScreen/SendAmountScreenTest.kt @@ -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) } + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/SolanaWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/SolanaWarningsTest.kt index ec60a4de62..976cfeb85f 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/SolanaWarningsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/SolanaWarningsTest.kt @@ -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) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ef63e16db7..ab376d6124 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ + android:required="false" /> diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 8db7434ef7..076a21b682 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 7804a24b4f..2815df1738 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -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), diff --git a/app/src/main/java/com/tangem/tap/common/analytics/DefaultTrackingContextProxy.kt b/app/src/main/java/com/tangem/tap/common/analytics/DefaultTrackingContextProxy.kt index a32012ca51..6174bb4473 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/DefaultTrackingContextProxy.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/DefaultTrackingContextProxy.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index a90e193a92..125b159021 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -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") diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt index a4fcb32fe0..a8deb0d88d 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt @@ -11,85 +11,5 @@ sealed class Onboarding( params: Map = emptyMap(), ) : AnalyticsEvent(category, event, params) { - class Started : Onboarding("Onboarding", "Onboarding Started") class Finished : Onboarding("Onboarding", "Onboarding Finished") - - sealed class CreateWallet( - event: String, - params: Map = 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 = 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 = 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 = 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), - ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Push.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Push.kt index c4345c597f..acdec4105e 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Push.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Push.kt @@ -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") } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt index 3ac65172d2..c303a9cbba 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt @@ -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", diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt index d34334ec34..21f5f23180 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt @@ -10,8 +10,6 @@ sealed class SignIn( params: Map = 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") } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt index d28504b612..1fdb08a185 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt index fbea91a9f4..7e7106ebb4 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt @@ -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) } } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index 1608ebc3f4..cdd42cdde9 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -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 } } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/HotWalletContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/HotWalletContextInterceptor.kt index b7be0155f7..f67eaab3f8 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/HotWalletContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/HotWalletContextInterceptor.kt @@ -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) { params[AnalyticsParam.PRODUCT_TYPE] = AnalyticsParam.ProductType.MobileWallet.value + params.remove(AnalyticsParam.BATCH) + params.remove(AnalyticsParam.FIRMWARE) + params.remove(AnalyticsParam.CURRENCY) } companion object { diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index 2afd20f31a..80d510d122 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -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, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt index 57fd8da9ae..8c8404b2d3 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt @@ -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) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index c4e44fe950..b1d7b0a206 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -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), ) diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 9b33e83a66..ef41aa0007 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -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), ) diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index 2ed2a31346..2fbc11170b 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/di/domain/NewsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NewsDomainModule.kt index dda3134a35..55b1457a55 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NewsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NewsDomainModule.kt @@ -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) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt index 0c20fd0993..34ffd4aaba 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -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, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 631f9a46a0..0280493a9a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -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, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index c9c0d08868..7528f0f18b 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -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, ) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index ac18c8b943..95d2b8fe04 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -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, ), ) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index e9bc518930..1c3cfc1d98 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -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, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index af0ce672e0..80dce033a7 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -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?>(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) { + private suspend fun removeHotWalletsFromSDKAndRepos(walletIds: List) { val hotWalletsToDelete = userWalletsSync() .filterIsInstance() .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, + ), + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt index e64db24760..e74499daf0 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt @@ -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() } } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 3a4901f8e3..e3e1923c6b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -196,7 +196,7 @@ class DetailsMiddleware { } private fun enrollBiometrics() { - Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication) + Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication()) store.inject(DaggerGraphState::settingsManager).openBiometricSettings() } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/analytics/AppSettingsItemsAnalyticsSender.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/analytics/AppSettingsItemsAnalyticsSender.kt index 64a5832a8c..520610a058 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/analytics/AppSettingsItemsAnalyticsSender.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/analytics/AppSettingsItemsAnalyticsSender.kt @@ -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 } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt index a81c2bdbd1..7cffbf079d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt @@ -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, + ) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt index 5f4415c577..a3422995a8 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt @@ -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 = 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 - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt index cc27cf74c8..d174d82415 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index e37cf7eac6..3e04e9381b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -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), diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt index 32eb289aec..68b8585c72 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt index fbeff5f6f4..df8cc61880 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt @@ -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))) } diff --git a/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt index 9ca6d43223..0d6b4cf157 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index f912f0948a..3fea04772f 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt index 7c7e43f2b5..5c3ddfe725 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt index de791e45da..50cfcc86b1 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt index f873c4683f..597915ff66 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt @@ -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") } diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt index 5839903010..5f54236082 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt @@ -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 } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index e51b49f725..65ece0eaad 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -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, + ), + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/transitions/RoutingTransitionAnimationFactory.kt b/app/src/main/java/com/tangem/tap/routing/transitions/RoutingTransitionAnimationFactory.kt index a28e2ff15e..f575967f52 100644 --- a/app/src/main/java/com/tangem/tap/routing/transitions/RoutingTransitionAnimationFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/transitions/RoutingTransitionAnimationFactory.kt @@ -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))) diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index a3a9cf4047..df8495fe6c 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -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, ), diff --git a/build.gradle.kts b/build.gradle.kts index b59181ab1b..c8864ff496 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,5 @@ import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import java.util.concurrent.ConcurrentHashMap plugins { alias(deps.plugins.kotlin.android) apply false @@ -32,21 +33,53 @@ interface Injected { 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() + +// 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 { tasks.withType().configureEach { - val taskName = name.lowercase() - 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") - } + println("Test task scheduled: $path") testLogging { exceptionFormat = TestExceptionFormat.FULL @@ -54,6 +87,13 @@ subprojects { afterSuite(KotlinClosure2({ desc, result -> 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 = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" 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 { diff --git a/common/routing/detekt-baseline-debug.xml b/common/routing/detekt-baseline-debug.xml deleted file mode 100644 index 84f08bf7e3..0000000000 --- a/common/routing/detekt-baseline-debug.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - BooleanPropertyNaming:AppRoute.kt$AppRoute.CreateWalletBackup$val setAccessCode: Boolean = false - NestedScopeFunctions:PayloadToDeeplinkConverter.kt$PayloadToDeeplinkConverter$let { addQueryParam(NAME_KEY, it) } - NestedScopeFunctions:PayloadToDeeplinkConverter.kt$PayloadToDeeplinkConverter$let { addQueryParam(TRANSACTION_ID_KEY, it) } - - diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 4e22012973..cbe8919317 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -346,7 +346,9 @@ sealed class AppRoute(val path: String) : Route { object CreateHardwareWallet : AppRoute(path = "/create_hardware_wallet") @Serializable - object CreateMobileWallet : AppRoute(path = "/create_mobile_wallet") + data class CreateMobileWallet( + val source: String, + ) : AppRoute(path = "/create_mobile_wallet") @Serializable data class UpgradeWallet( @@ -368,7 +370,7 @@ sealed class AppRoute(val path: String) : Route { val analyticsSource: String, val analyticsAction: String, val isUpgradeFlow: Boolean = false, - val setAccessCode: Boolean = false, + val shouldSetAccessCode: Boolean = false, ) : AppRoute(path = "/create_wallet_backup/${userWalletId.stringValue}") @Serializable diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/PayloadToDeeplinkConverter.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/PayloadToDeeplinkConverter.kt index ff88b96b22..5bee03bf9b 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/PayloadToDeeplinkConverter.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/PayloadToDeeplinkConverter.kt @@ -55,8 +55,12 @@ object PayloadToDeeplinkConverter : Converter, String?> { addQueryParam(DERIVATION_PATH_KEY, derivationPath) } - transactionId?.let { addQueryParam(TRANSACTION_ID_KEY, it) } - name?.let { addQueryParam(NAME_KEY, it) } + if (transactionId != null) { + addQueryParam(TRANSACTION_ID_KEY, transactionId) + } + if (name != null) { + addQueryParam(NAME_KEY, name) + } }.build() } diff --git a/common/test/src/main/java/com/tangem/common/test/data/staking/MockP2PEthPoolAccountResponseFactory.kt b/common/test/src/main/java/com/tangem/common/test/data/staking/MockP2PEthPoolAccountResponseFactory.kt new file mode 100644 index 0000000000..1e814c5d9f --- /dev/null +++ b/common/test/src/main/java/com/tangem/common/test/data/staking/MockP2PEthPoolAccountResponseFactory.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/common/ui/detekt-baseline-debug.xml b/common/ui/detekt-baseline-debug.xml index 76624db4ad..13ac4c3486 100644 --- a/common/ui/detekt-baseline-debug.xml +++ b/common/ui/detekt-baseline-debug.xml @@ -18,9 +18,7 @@ CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$minimumSendAmount: BigDecimal? CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$rentWarning: CryptoCurrencyWarning.Rent? MultilineLambdaItParameter:ExpressStatusItems.kt${ val itemInfo = expressTxs[it].info val (iconRes, tint) = when (itemInfo.iconState) { ExpressTransactionStateIconUM.Warning -> { R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention } ExpressTransactionStateIconUM.Error -> { R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning } ExpressTransactionStateIconUM.None -> 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(), ) } - MultilineLambdaItParameter:TokenItemStateConverter.kt$TokenItemStateConverter${ createTitleState(it, yieldModuleApyMap, stakingApyMap, { onApyLabelClick?.invoke(it) }) } MultilineLambdaItParameter:TokenItemStateConverter.kt$TokenItemStateConverter.Companion${ it.key.equals( other = token.yieldSupplyKey(), ignoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId), ) } - NamedArguments:TokenItemStateConverter.kt$TokenItemStateConverter$createTitleState(it, yieldModuleApyMap, stakingApyMap, { onApyLabelClick?.invoke(it) }) NoNameShadowing:NavigationButtonsBlock.kt$navigationUM NoNameShadowing:UserWalletItem.kt$balance NullableBooleanCheck:TokenItemStateConverter.kt$TokenItemStateConverter.Companion$cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive ?: false diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt index 0932e007ab..dbc955ab1e 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt @@ -271,6 +271,7 @@ private fun AmountFieldError( style = TangemTheme.typography.caption2, color = color, textAlign = TextAlign.Center, + modifier = Modifier.testTag(SendScreenTestTags.AMOUNT_ERROR_TEXT), ) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleBadge.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleBadge.kt deleted file mode 100644 index 157a36e08d..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleBadge.kt +++ /dev/null @@ -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")), - ) - } - } -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt index 259c72b98d..70d50e3b81 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt @@ -1,41 +1,47 @@ package com.tangem.common.ui.news import android.content.res.Configuration -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CardColors import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember 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.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.R 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.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.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.utils.StringsSigns import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet -import kotlinx.collections.immutable.toPersistentList @Composable -fun ArticleCard(articleConfigUM: ArticleConfigUM, onArticleClick: () -> Unit, modifier: Modifier = Modifier) { +fun ArticleCard( + articleConfigUM: ArticleConfigUM, + onArticleClick: () -> Unit, + modifier: Modifier = Modifier, + colors: CardColors = TangemBlockCardColors, +) { BlockCard( modifier = modifier, onClick = onArticleClick, + colors = colors, ) { if (articleConfigUM.isTrending) { 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) @Composable -private fun Tags(tags: ImmutableList, modifier: Modifier = Modifier) { +private fun Tags(tags: ImmutableList, modifier: Modifier = Modifier) { val expandIndicator = remember { ContextualFlowRowOverflow.expandIndicator { val remainingItems = tags.size - shownItemCount - ArticleBadge(articleTagUM = ArticleTagUM.Category(TextReference.Str("${StringsSigns.PLUS}$remainingItems"))) + Label(state = LabelUM(TextReference.Str("${StringsSigns.PLUS}$remainingItems"))) } } ContextualFlowRow( @@ -179,7 +144,7 @@ private fun Tags(tags: ImmutableList, modifier: Modifier = Modifie maxLines = 1, overflow = expandIndicator, ) { index -> - ArticleBadge(articleTagUM = tags[index]) + Label(state = tags[index]) } } @@ -189,14 +154,14 @@ private fun Tags(tags: ImmutableList, modifier: Modifier = Modifie private fun TagsPreview() { TangemThemePreview { Tags( - tags = listOf( - ArticleTagUM.Category(TextReference.Str("Hype")), - ArticleTagUM.Category(TextReference.Str("BTC")), - ArticleTagUM.Category(TextReference.Str("Supply")), - ArticleTagUM.Category(TextReference.Str("Demand")), - ArticleTagUM.Category(TextReference.Str("Best rate")), - ArticleTagUM.Category(TextReference.Str("Breaking news")), - ).toPersistentList(), + tags = persistentListOf( + LabelUM(TextReference.Str("Hype")), + LabelUM(TextReference.Str("BTC")), + LabelUM(TextReference.Str("Supply")), + LabelUM(TextReference.Str("Demand")), + LabelUM(TextReference.Str("Best rate")), + LabelUM(TextReference.Str("Breaking news")), + ), ) } } @@ -206,12 +171,11 @@ private fun TagsPreview() { @Composable private fun ArticleCardsPreview() { val tags = listOf( - ArticleTagUM.Category(TextReference.Str("Hype")), - ArticleTagUM.Category(TextReference.Str("BTC")), - ArticleTagUM.Category(TextReference.Str("Supply")), - ArticleTagUM.Category(TextReference.Str("Demand")), - ArticleTagUM.Category(TextReference.Str("Best rate")), - ArticleTagUM.Category(TextReference.Str("Breaking news")), + LabelUM(TextReference.Str("Hype")), + LabelUM(TextReference.Str("BTC")), + LabelUM(TextReference.Str("Supply")), + LabelUM(TextReference.Str("Demand")), + LabelUM(TextReference.Str("Breaking news")), ).toImmutableSet() val config = ArticleConfigUM( diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt index e6e9477790..46e616eaab 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt @@ -1,5 +1,6 @@ package com.tangem.common.ui.news +import com.tangem.core.ui.components.label.entity.LabelUM import kotlinx.collections.immutable.ImmutableSet data class ArticleConfigUM( @@ -8,6 +9,6 @@ data class ArticleConfigUM( val score: Float, val createdAt: String, val isTrending: Boolean, - val tags: ImmutableSet, + val tags: ImmutableSet, val isViewed: Boolean, ) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt new file mode 100644 index 0000000000..15bac87bdc --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt @@ -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, + 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, + ) + } + } + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt new file mode 100644 index 0000000000..f7cc656eae --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt new file mode 100644 index 0000000000..4ef5f1ce75 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleTagUM.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleTagUM.kt deleted file mode 100644 index 24eb867ad2..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleTagUM.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index ce46ff400f..8408463879 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -21,7 +21,7 @@ import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus 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.stakekit.Yield import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance @@ -171,7 +171,7 @@ class TokenItemStateConverter( 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() private fun createTitleState( @@ -278,12 +278,13 @@ class TokenItemStateConverter( val validators = stakingApyMap[stakingKey] ?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null) - val yieldBalance = currencyStatus.value.yieldBalance - val hasStakedBalance = yieldBalance is YieldBalance.Data + val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data + val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit - val rateInfo: Pair? = if (hasStakedBalance) { + val rateInfo: Pair? = if (stakeKitBalance != null) { + // StakeKit-specific: try to find rate from validator address val validatorsByAddress = validators.associateBy { it.address } - yieldBalance.balance.items + stakeKitBalance.balance.items .mapNotNull { it.validatorAddress } .firstNotNullOfOrNull { address -> val validator = validatorsByAddress[address] @@ -298,6 +299,8 @@ class TokenItemStateConverter( } .maxByOrNull { it.first } } else { + // P2P or no balance: use preferred validators + // TODO p2p validators .filter { it.preferred } .mapNotNull { validator -> @@ -310,7 +313,7 @@ class TokenItemStateConverter( return StakingLocalInfo( rate = rateInfo?.first, - isActive = hasStakedBalance, + isActive = stakingBalance != null, rewardType = rateInfo?.second, ) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt index 851972f960..e7e4b2a884 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt @@ -1,6 +1,8 @@ package com.tangem.common.ui.userwallet 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.resourceReference 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( onAlreadyUnlocked: () -> Unit = {}, onUserCancelled: () -> Unit = {}, + analyticsEventHandler: AnalyticsEventHandler, noinline showMessage: (EventMessage) -> Unit, ) { 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 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) { is UnlockWalletError.UnableToUnlock.WithReason -> { when (error.reason) { Reason.AllKeysInvalidated -> { + analyticsEventHandler.send(SignIn.ErrorBiometricUpdated()) DialogMessage( title = resourceReference(R.string.biometric_updated_warning_title), message = resourceReference(R.string.biometric_updated_warning_description), diff --git a/core/analytics/models/detekt-baseline-main.xml b/core/analytics/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..c8cb8b3d13 --- /dev/null +++ b/core/analytics/models/detekt-baseline-main.xml @@ -0,0 +1,17 @@ + + + + + MultilineLambdaItParameter:TechAnalyticsEvent.kt$TechAnalyticsEvent.KeyboardIdentifier${ put("Package", it) put("GPUrl", "https://play.google.com/store/apps/details?id=$packageName") } + UseEmptyCounterpart:AnalyticsEvent.kt$AnalyticsEvent$mapOf() + UseEmptyCounterpart:Basic.kt$Basic$mapOf() + UseEmptyCounterpart:ExceptionAnalyticsEvent.kt$ExceptionAnalyticsEvent$mapOf() + UseEmptyCounterpart:MainScreenAnalyticsEvent.kt$MainScreenAnalyticsEvent$mapOf() + UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent$mapOf() + UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent.CreateWallet$mapOf() + UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent.Error$mapOf() + UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent.Onboarding$mapOf() + UseEmptyCounterpart:OnboardingAnalyticsEvent.kt$OnboardingAnalyticsEvent.SeedPhrase$mapOf() + UseEmptyCounterpart:TechAnalyticsEvent.kt$TechAnalyticsEvent$mapOf() + + diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 47d586e92e..9488b9c840 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -11,11 +11,6 @@ sealed class AnalyticsParam { companion object } - sealed class TokenBalanceState(val value: String) { - data object Empty : TokenBalanceState("Empty") - data object Full : TokenBalanceState("Full") - } - sealed class RateApp(val value: String) { data object Liked : RateApp("Liked") data object Disliked : RateApp("Disliked") @@ -83,12 +78,14 @@ sealed class AnalyticsParam { data object Onboarding : ScreensSources("Onboarding") data object LongTap : ScreensSources("Long Tap") data object Markets : ScreensSources("Markets") - data object HotWallet : ScreensSources("Hot Wallet") data object TangemPay : ScreensSources("Tangem Pay") data object WalletSettings : ScreensSources("Wallet Settings") data object Upgrade : ScreensSources("Upgrade") data object HardwareWallet : ScreensSources("Hardware 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) { @@ -203,8 +200,9 @@ sealed class AnalyticsParam { Pending(value = "Pending"), } - enum class EnsStatus(val value: String) { - EMPTY("Empty"), FULL("Full") + enum class EmptyFull(val value: String) { + Empty("Empty"), + Full("Full"), } enum class ProductType(val value: String) { @@ -268,5 +266,6 @@ sealed class AnalyticsParam { const val CHOSEN_TOKEN = "Token Chosen" const val ENS = "ENS" const val ENS_ADDRESS = "ENS Address" + const val ACCOUNT_DERIVATION_FROM = "Account Derivation (from)" } } \ No newline at end of file diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt index 9d2974a077..c59513d2fa 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt @@ -10,11 +10,11 @@ sealed class Basic( ) : Basic( event = "Card Was Scanned", params = mapOf( - AnalyticsParam.SOURCE to source.value, + AnalyticsParam.Key.SOURCE to source.value, ), ) - class SignedIn( + class SignedInLegacy( currency: AnalyticsParam.WalletType, batch: String, signInType: SignInType, @@ -24,8 +24,8 @@ sealed class Basic( ) : Basic( event = "Signed in", params = buildMap { - put(AnalyticsParam.CURRENCY, currency.value) - put(AnalyticsParam.BATCH, batch) + put(AnalyticsParam.Key.CURRENCY, currency.value) + put(AnalyticsParam.Key.BATCH, batch) put("Wallet Type", if (isImported) "Seed Phrase" else "Seedless") put("Sign in type", signInType.name) 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) : Basic( event = "Topped up", - params = mapOf(AnalyticsParam.CURRENCY to currency.value), + params = mapOf(AnalyticsParam.Key.CURRENCY to currency.value), ), OneTimeAnalyticsEvent { @@ -53,16 +80,16 @@ sealed class Basic( Basic( event = "Transaction sent", params = buildMap { - this[AnalyticsParam.SOURCE] = sentFrom.value + this[AnalyticsParam.Key.SOURCE] = sentFrom.value if (sentFrom is AnalyticsParam.TxData) { - this[AnalyticsParam.BLOCKCHAIN] = sentFrom.blockchain - this[AnalyticsParam.TOKEN_PARAM] = sentFrom.token + this[AnalyticsParam.Key.BLOCKCHAIN] = sentFrom.blockchain + this[AnalyticsParam.Key.TOKEN_PARAM] = sentFrom.token sentFrom.feeType?.value?.let { - this[AnalyticsParam.FEE_TYPE] = it + this[AnalyticsParam.Key.FEE_TYPE] = it } } if (sentFrom is AnalyticsParam.TxSentFrom.Approve) { - this[AnalyticsParam.PERMISSION_TYPE] = sentFrom.permissionType + this[AnalyticsParam.Key.PERMISSION_TYPE] = sentFrom.permissionType } this["Memo"] = memoType.name }, @@ -79,7 +106,7 @@ sealed class Basic( class ButtonSupport(source: AnalyticsParam.ScreensSources) : Basic( event = "Request Support", params = mapOf( - AnalyticsParam.SOURCE to source.value, + AnalyticsParam.Key.SOURCE to source.value, ), ) @@ -89,7 +116,7 @@ sealed class Basic( ) : Basic( event = "Biometry Failed", params = mapOf( - AnalyticsParam.SOURCE to source.value, + AnalyticsParam.Key.SOURCE to source.value, "Reason" to reason.value, ), ) { diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt index c8864d3617..b511dd504e 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt @@ -36,26 +36,34 @@ sealed class MainScreenAnalyticsEvent( }, ) - data object ButtonReceive : MainScreenAnalyticsEvent( + class ButtonReceive : MainScreenAnalyticsEvent( event = "Button - Receive", ) - data object LimitsClicked : MainScreenAnalyticsEvent( + class LimitsClicked : MainScreenAnalyticsEvent( event = "Limits Clicked", ) - data object NoticeBalancesInfo : MainScreenAnalyticsEvent( + class NoticeBalancesInfo : MainScreenAnalyticsEvent( event = "Notice - Balances Info", ) - data object NoticeLimitsInfo : MainScreenAnalyticsEvent( + class NoticeLimitsInfo : MainScreenAnalyticsEvent( event = "Notice - Limits Info", ) - data object ButtonExplore : MainScreenAnalyticsEvent( + class ButtonExplore : MainScreenAnalyticsEvent( 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( event = "Button - Swap", params = mapOf(AnalyticsParam.STATUS to status.value), @@ -66,11 +74,11 @@ sealed class MainScreenAnalyticsEvent( 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( event = "Buy Token Clicked", diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt index f60f3b53fe..f4bc6d2ca5 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt @@ -12,11 +12,92 @@ sealed class OnboardingAnalyticsEvent( sealed class Onboarding( event: String, params: Map = 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 = 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 = 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 = mapOf(), ) : OnboardingAnalyticsEvent(category = "Error", event = event, params = params) { data class OfflineAttestationFailed( val source: AnalyticsParam.ScreensSources, - ) : Onboarding( + ) : Error( event = "Offline Attestation Failed", params = mapOf(AnalyticsParam.SOURCE to source.value), ) diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SignIn.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SignIn.kt new file mode 100644 index 0000000000..9b88374046 --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SignIn.kt @@ -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 = 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, + ), + ) +} \ No newline at end of file diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 03e0a8cfd3..cd6aa8ac62 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -33,7 +33,7 @@ }, { "name": "HOT_WALLET_ENABLED", - "version": "undefined" + "version": "5.32.0" }, { "name": "TANGEM_PAY_ENABLED", diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt index 755887fd7f..f976466d89 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt @@ -22,6 +22,9 @@ enum class ApiEnvironment { @Json(name = "STAGE") STAGE, + @Json(name = "STAGE_2") + STAGE_2, + @Json(name = "MOCK") MOCK, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt index 6d4b9a1e5a..4104dae025 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt @@ -30,6 +30,7 @@ internal class Express( createDev2Environment(), createDev3Environment(), createStageEnvironment(), + createStage2Environment(), createMockedEnvironment(), createProdEnvironment(), ) @@ -73,6 +74,12 @@ internal class Express( 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( environment = ApiEnvironment.MOCK, baseUrl = "[REDACTED_ENV_URL]", diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/P2PEthPool.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/P2PEthPool.kt index b699172b24..aa1b28c629 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/P2PEthPool.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/P2PEthPool.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.api.common.config import com.tangem.datasource.BuildConfig +import com.tangem.domain.staking.model.ethpool.P2PStakingConfig import com.tangem.lib.auth.P2PEthPoolAuthProvider import com.tangem.utils.ProviderSuspend @@ -22,12 +23,7 @@ internal class P2PEthPool( private fun getInitialEnvironment(): ApiEnvironment { return when (BuildConfig.BUILD_TYPE) { MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK - DEBUG_BUILD_TYPE, - INTERNAL_BUILD_TYPE, - EXTERNAL_BUILD_TYPE, - RELEASE_BUILD_TYPE, - -> ApiEnvironment.PROD - else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") + else -> if (P2PStakingConfig.USE_TESTNET) ApiEnvironment.DEV else ApiEnvironment.PROD } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt index 6b7d1ab493..b5e7ea38c6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt @@ -77,6 +77,7 @@ internal class YieldSupply( ApiEnvironment.DEV_2, ApiEnvironment.DEV_3, ApiEnvironment.STAGE, + ApiEnvironment.STAGE_2, -> environmentConfigStorage.getConfigSync().yieldModuleApiKeyDev ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().yieldModuleApiKey } ?: error("No tangem tech api config provided") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt index 60503742bb..cf3ab2f3e5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt @@ -23,9 +23,7 @@ interface P2PEthPoolApi { * @param network Ethereum pool network: "mainnet" or "hoodi" (testnet) */ @GET("api/v1/staking/pool/{network}/vaults") - suspend fun getVaults( - @Path("network") network: String = "mainnet", - ): ApiResponse> + suspend fun getVaults(@Path("network") network: String): ApiResponse> /** * Prepare deposit transaction diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/news/NewsApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/news/NewsApi.kt index 24e359b20e..d0e5ab9707 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/news/NewsApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/news/NewsApi.kt @@ -38,6 +38,6 @@ interface NewsApi { private companion object { - private const val NEWS_PATH = "api/v1/news" + private const val NEWS_PATH = "v1/news" } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsTrendingResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsTrendingResponse.kt index 3a21af3d04..91464cb405 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsTrendingResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsTrendingResponse.kt @@ -5,11 +5,5 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class NewsTrendingResponse( - @Json(name = "meta") val meta: NewsTrendingMetaDto, @Json(name = "items") val items: List, -) - -@JsonClass(generateAdapter = true) -data class NewsTrendingMetaDto( - @Json(name = "limit") val limit: Int, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index d3e09a5f52..aec86d12d2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -50,6 +50,12 @@ interface TangemTechApi { @Body userTokens: UserTokensResponse, ): ApiResponse + @PUT("/v1/wallets/{walletId}/tokens") + suspend fun saveTokens( + @Path(value = "walletId") userId: String, + @Body userTokens: UserTokensResponse, + ): ApiResponse + /** Returns referral status by [walletId] */ @GET("v1/referral/{walletId}") suspend fun getReferralStatus(@Path("walletId") walletId: String): ApiResponse @@ -129,6 +135,12 @@ interface TangemTechApi { @Body body: List, ): ApiResponse + @PUT("/v1/user-wallets/applications/{application_id}/wallets") + suspend fun associateApplicationIdWithWalletsV2( + @Path("application_id") applicationId: String, + @Body body: AssociateApplicationIdWithWalletsBody, + ): ApiResponse + @GET("v1/user-wallets/wallets/{wallet_id}") suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateAppWithWalletsErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateAppWithWalletsErrorResponse.kt new file mode 100644 index 0000000000..14696568f4 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateAppWithWalletsErrorResponse.kt @@ -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, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateApplicationIdWithWalletsBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateApplicationIdWithWalletsBody.kt new file mode 100644 index 0000000000..59dfa30b85 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/AssociateApplicationIdWithWalletsBody.kt @@ -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, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt index 757436e0fc..6b484534d2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt @@ -19,6 +19,7 @@ data class GetWalletAccountsResponse( @Json(name = "group") val group: GroupType?, @Json(name = "sort") val sort: SortType?, @Json(name = "totalAccounts") val totalAccounts: Int, + @Json(name = "totalArchivedAccounts") val totalArchivedAccounts: Int, ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt index 5f9e2c12a9..45e05a8df4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/AppCurrencyResponseStore.kt @@ -15,4 +15,7 @@ interface AppCurrencyResponseStore { /** Get [CurrenciesResponse.Currency] synchronously or null */ suspend fun getSyncOrNull(): CurrenciesResponse.Currency? + + /** Store [CurrenciesResponse.Currency] */ + suspend fun store(currency: CurrenciesResponse.Currency) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt index 1c268c06c7..9225bd5ab8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/appcurrency/DefaultAppCurrencyResponseStore.kt @@ -5,6 +5,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObject import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.preferences.utils.storeObject import kotlinx.coroutines.flow.Flow /** @@ -25,4 +26,11 @@ internal class DefaultAppCurrencyResponseStore( PreferencesKeys.SELECTED_APP_CURRENCY_KEY, ) } + + override suspend fun store(currency: CurrenciesResponse.Currency) { + appPreferencesStore.storeObject( + PreferencesKeys.SELECTED_APP_CURRENCY_KEY, + currency, + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt index b8af7137ae..af846ebcbb 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/PromoStoreModule.kt @@ -1,7 +1,10 @@ package com.tangem.datasource.di 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.PromoBannerStore import com.tangem.datasource.local.promo.PromoStoriesStore import dagger.Module import dagger.Provides @@ -18,4 +21,10 @@ object PromoStoreModule { fun providePromoStoriesStore(): PromoStoriesStore { return DefaultPromoStoriesStore(dataStore = RuntimeDataStore()) } + + @Provides + @Singleton + fun providePromoBannerStore(): PromoBannerStore { + return DefaultPromoBannerStore(dataStore = RuntimeSharedStore()) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt index 695c1c8d45..aeb4d1b8d9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt @@ -5,6 +5,7 @@ import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile 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.YieldDTO import com.tangem.datasource.local.datastore.RuntimeDataStore @@ -77,6 +78,24 @@ internal object StakingStoreModule { return DefaultStakingActionsStore(dataStore = RuntimeDataStore()) } + @Provides + @Singleton + fun provideP2PBalancesPersistenceStore( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + dispatchers: CoroutineDispatcherProvider, + ): DataStore>> { + return DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = mapWithStringKeyTypes(valueTypes = setTypes()), + defaultValue = emptyMap(), + ), + produceFile = { context.dataStoreFile(fileName = "p2p_balances") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ) + } + @Provides @Singleton fun provideP2PEthPoolVaultsStore( diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/promo/DefaultPromoBannerStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/promo/DefaultPromoBannerStore.kt new file mode 100644 index 0000000000..6065fa079a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/promo/DefaultPromoBannerStore.kt @@ -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>, +) : 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) + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/promo/PromoBannerStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/promo/PromoBannerStore.kt new file mode 100644 index 0000000000..f951238bd5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/promo/PromoBannerStore.kt @@ -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) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingBalanceConverter.kt similarity index 82% rename from core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingBalanceConverter.kt index 828449590f..9cb1a6655c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingBalanceConverter.kt @@ -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.domain.models.StatusSource 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.YieldBalance import com.tangem.domain.models.staking.YieldBalanceItem import com.tangem.utils.converter.Converter 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, -) : Converter { +) : Converter { 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( integrationId = value.integrationId ?: return null, address = value.addresses.address, ) return if (value.balances.isEmpty()) { - YieldBalance.Empty(stakingId = stakingId, source = source) + StakingBalance.Empty(stakingId = stakingId, source = source) } else { - YieldBalance.Data( + StakingBalance.Data.StakeKit( stakingId = stakingId, balance = YieldBalanceItem( items = value.balances diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index 37f48a1f83..bf6b755bc5 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -12,6 +12,7 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_ import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY +import com.tangem.domain.staking.model.ethpool.P2PStakingConfig import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.P2PEthPoolAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider @@ -299,11 +300,17 @@ internal class ProdApiConfigsManagerTest { } private fun createP2PModel(): TestModel { + val (environment, baseUrl) = if (P2PStakingConfig.USE_TESTNET) { + ApiEnvironment.DEV to "https://api-test.p2p.org/" + } else { + ApiEnvironment.PROD to "https://api.p2p.org/" + } + return TestModel( id = ApiConfig.ID.P2PEthPool, expected = ApiEnvironmentConfig( - environment = ApiEnvironment.PROD, - baseUrl = "https://api.p2p.org/", + environment = environment, + baseUrl = baseUrl, headers = mapOf( "Authorization" to ProviderSuspend { "Bearer $P2P_API_KEY" }, "accept" to ProviderSuspend { "application/json" }, diff --git a/core/pagination/detekt-baseline-main.xml b/core/pagination/detekt-baseline-main.xml new file mode 100644 index 0000000000..5257c3fbcb --- /dev/null +++ b/core/pagination/detekt-baseline-main.xml @@ -0,0 +1,22 @@ + + + + + BooleanPropertyNaming:BatchAction.kt$BatchAction.UpdateBatches$val async: Boolean = false + BooleanPropertyNaming:BatchFetchResult.kt$BatchFetchResult.Success$val empty: Boolean + BooleanPropertyNaming:BatchFetchResult.kt$BatchFetchResult.Success$val last: Boolean + BooleanPropertyNaming:BatchListSource.kt$DefaultBatchListSource$val started = job.start() + MultilineLambdaItParameter:BatchListSource.kt$DefaultBatchListSource${ currentCoroutineContext().ensureActive() BatchFetchResult.Error(it) } + MultilineLambdaItParameter:BatchListSource.kt$DefaultBatchListSource${ if (predicate(it.first)) { it.second.cancel() null } else { it } } + MultilineLambdaItParameter:CursorBatchFetcher.kt$CursorBatchFetcher${ currentCoroutineContext().ensureActive() return BatchFetchResult.Error(it) } + MultilineLambdaItParameter:LimitOffsetBatchFetcher.kt$LimitOffsetBatchFetcher${ currentCoroutineContext().ensureActive() BatchFetchResult.Error(it) } + NamedArguments:BatchListSource.kt$DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, null) + NamedArguments:BatchListSource.kt$DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, updateFetcher) + NestedScopeFunctions:BatchListSource.kt$DefaultBatchListSource$also { currentCoroutineContext().ensureActive() } + SuspendFunSwallowedCancellation:BatchListSource.kt$DefaultBatchListSource$runCatching + SuspendFunSwallowedCancellation:CursorBatchFetcher.kt$CursorBatchFetcher$runCatching + SuspendFunSwallowedCancellation:LimitOffsetBatchFetcher.kt$LimitOffsetBatchFetcher$runCatching + UseEmptyCounterpart:BatchListSource.kt$DefaultBatchListSource$listOf() + UseOrEmpty:BatchListSource.kt$DefaultBatchListSource$batch?.let { listOf(it) } ?: emptyList() + + diff --git a/core/ui/detekt-baseline-debug.xml b/core/ui/detekt-baseline-debug.xml index a625c6029f..662bc3318f 100644 --- a/core/ui/detekt-baseline-debug.xml +++ b/core/ui/detekt-baseline-debug.xml @@ -24,7 +24,6 @@ NoNameShadowing:TextAnimatedCounter.kt$char PropertyUsedBeforeDeclaration:InputManager.kt$InputManager$_query ReusedModifierInstance:EllipsisText.kt$Text( text = layoutText, color = color, style = style, fontStyle = fontStyle, textDecoration = textDecoration, textAlign = textAlign, softWrap = softWrap, maxLines = 1, onTextLayout = { textLayoutResultState.value = it }, modifier = modifier, ) - ReusedModifierInstance:Label.kt$Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = modifier .padding(horizontal = 4.dp) .clip(TangemTheme.shapes.roundedCorners8) .background(color = backgroundColor) .then( if (state.onClick != null) { Modifier.clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), onClick = state.onClick, ) } else { Modifier }, ) .padding(horizontal = 8.dp, vertical = 4.dp), ) { Text( modifier = Modifier.weight(1.0f, fill = false), text = text.resolveReference(), style = TangemTheme.typography.caption1, color = textColor, ) AnimatedVisibility(state.icon != null) { val wrappedIcon = remember(this) { requireNotNull(state.icon) } Icon( imageVector = ImageVector.vectorResource(wrappedIcon), tint = iconColor, contentDescription = null, modifier = Modifier .size(16.dp) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(bounded = false), onClick = { state.onIconClick?.invoke() }, ), ) } } ReusedModifierInstance:TangemRadioButton.kt$AnimatedVisibility( visible = isSelected, label = "Radio button animation", modifier = modifier .size(TangemTheme.dimens.size24), ) { Icon( painter = painterResource(id = R.drawable.ic_check_circle_24), contentDescription = null, tint = TangemTheme.colors.control.checked, ) } ReusedModifierInstance:TokenPrice.kt$Icon( modifier = modifier, painter = painterResource( id = when (animatedType) { PriceChangeType.UP -> R.drawable.ic_arrow_up_8 PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8 PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8 }, ), tint = when (animatedType) { PriceChangeType.UP -> TangemTheme.colors.icon.accent PriceChangeType.DOWN -> TangemTheme.colors.icon.warning PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive }, contentDescription = null, ) UnnecessaryEventHandlerParameter:PinTextField.kt$onValueChange: (String) -> Unit diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index 7b4e6dbb70..00b71108b4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -220,6 +220,8 @@ fun SecondaryButtonIconEnd( modifier: Modifier = Modifier, showProgress: Boolean = false, enabled: Boolean = true, + size: TangemButtonSize = TangemButtonSize.Default, + shape: Shape = size.toShape(), ) { TangemButton( modifier = modifier, @@ -230,6 +232,8 @@ fun SecondaryButtonIconEnd( enabled = enabled, showProgress = showProgress, textStyle = TangemTheme.typography.subtitle1, + size = size, + shape = shape, ) } @@ -244,6 +248,8 @@ fun SecondaryButtonIconStart( modifier: Modifier = Modifier, showProgress: Boolean = false, enabled: Boolean = true, + size: TangemButtonSize = TangemButtonSize.Default, + shape: Shape = size.toShape(), ) { TangemButton( modifier = modifier, @@ -254,6 +260,8 @@ fun SecondaryButtonIconStart( enabled = enabled, showProgress = showProgress, textStyle = TangemTheme.typography.subtitle1, + size = size, + shape = shape, ) } // endregion SecondaryButton diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/BottomSheetState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/state/BottomSheetState.kt similarity index 51% rename from features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/BottomSheetState.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/state/BottomSheetState.kt index 9b58eb7690..70d201ddd5 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/BottomSheetState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/state/BottomSheetState.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.entry +package com.tangem.core.ui.components.bottomsheets.state enum class BottomSheetState { EXPANDED, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt b/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt index 2cc6209978..0f6b07e0d1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.ripple @@ -18,10 +19,16 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest import com.tangem.core.ui.R +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM +import com.tangem.core.ui.components.label.entity.LabelSize import com.tangem.core.ui.components.label.entity.LabelStyle import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference @@ -37,6 +44,7 @@ import com.tangem.core.ui.res.TangemThemePreview * * @see Figma */ +@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun Label(state: LabelUM, modifier: Modifier = Modifier) { val backgroundColor by animateColorAsState( @@ -63,12 +71,28 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) { }, ) - AnimatedContent(targetState = state.text) { text -> + val horizontalArrangementSize = remember { + when (state.size) { + LabelSize.REGULAR -> 4.dp + LabelSize.BIG -> 8.dp + } + } + + val paddings = remember { + when (state.size) { + LabelSize.REGULAR -> PaddingValues(horizontal = 8.dp, vertical = 4.dp) + LabelSize.BIG -> PaddingValues(horizontal = 16.dp, vertical = 8.dp) + } + } + + AnimatedContent( + modifier = modifier, + targetState = state.text, + ) { text -> Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = modifier - .padding(horizontal = 4.dp) + horizontalArrangement = Arrangement.spacedBy(horizontalArrangementSize), + modifier = Modifier .clip(TangemTheme.shapes.roundedCorners8) .background(color = backgroundColor) .then( @@ -82,8 +106,34 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) { Modifier }, ) - .padding(horizontal = 8.dp, vertical = 4.dp), + .padding(paddings), ) { + state.leadingContent.let { leadingContentUM -> + when (leadingContentUM) { + is LabelLeadingContentUM.Token -> { + SubcomposeAsyncImage( + modifier = Modifier.size(16.dp), + model = ImageRequest.Builder(context = LocalContext.current) + .data(leadingContentUM.iconUrl) + .crossfade(enable = true) + .allowHardware(enable = false) + .build(), + loading = { CircleShimmer() }, + error = { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.background.tertiary, + shape = CircleShape, + ), + ) + }, + contentDescription = null, + ) + } + LabelLeadingContentUM.None -> Unit + } + } Text( modifier = Modifier.weight(1.0f, fill = false), text = text.resolveReference(), @@ -109,6 +159,8 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) { } } +@Suppress("LongMethod") +@OptIn(ExperimentalLayoutApi::class) @Preview(showBackground = true) @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -118,47 +170,93 @@ private fun LabelPreview() { verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(16.dp), ) { - Label( - state = LabelUM( - text = TextReference.Str("Regular Label"), - style = LabelStyle.REGULAR, - ), - ) - Label( - state = LabelUM( - text = TextReference.Str("Accent Label"), - style = LabelStyle.ACCENT, - ), - ) - Label( - state = LabelUM( - text = TextReference.Str("Warning Label"), - style = LabelStyle.WARNING, - ), - ) - Label( - state = LabelUM( - text = TextReference.Str( - "Regular long long long long long long long long long long long long Label", + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Label( + state = LabelUM( + text = TextReference.Str("Regular Label"), + style = LabelStyle.REGULAR, ), - style = LabelStyle.REGULAR, - icon = R.drawable.ic_information_24, - ), - ) - Label( - state = LabelUM( - text = TextReference.Str("Accent Label"), - style = LabelStyle.ACCENT, - icon = R.drawable.ic_information_24, - ), - ) - Label( - state = LabelUM( - text = TextReference.Str("Warning Label"), - style = LabelStyle.WARNING, - icon = R.drawable.ic_information_24, - ), - ) + ) + Label( + state = LabelUM( + leadingContent = LabelLeadingContentUM.Token( + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/euro-coin.png", + ), + text = TextReference.Str("Regular Label"), + style = LabelStyle.REGULAR, + ), + ) + Label( + state = LabelUM( + text = TextReference.Str("Accent Label"), + style = LabelStyle.ACCENT, + ), + ) + Label( + state = LabelUM( + text = TextReference.Str("Warning Label"), + style = LabelStyle.WARNING, + ), + ) + } + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Label( + state = LabelUM( + text = TextReference.Str( + "Regular long long long long long long long long long long long long Label", + ), + style = LabelStyle.REGULAR, + icon = R.drawable.ic_information_24, + ), + ) + Label( + state = LabelUM( + text = TextReference.Str("Accent Label"), + style = LabelStyle.ACCENT, + icon = R.drawable.ic_information_24, + ), + ) + Label( + state = LabelUM( + text = TextReference.Str("Warning Label"), + style = LabelStyle.WARNING, + icon = R.drawable.ic_information_24, + ), + ) + } + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Label( + state = LabelUM( + text = TextReference.Str("Regular Label"), + style = LabelStyle.REGULAR, + size = LabelSize.BIG, + ), + ) + Label( + state = LabelUM( + text = TextReference.Str("Accent Label"), + style = LabelStyle.ACCENT, + size = LabelSize.BIG, + icon = R.drawable.ic_information_24, + ), + ) + Label( + state = LabelUM( + text = TextReference.Str("Warning Label"), + style = LabelStyle.WARNING, + size = LabelSize.BIG, + ), + ) + } } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt index 9982a897b0..131469bd96 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt @@ -1,16 +1,29 @@ package com.tangem.core.ui.components.label.entity import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference data class LabelUM( val text: TextReference, - val style: LabelStyle, + val style: LabelStyle = LabelStyle.REGULAR, + val size: LabelSize = LabelSize.REGULAR, + val leadingContent: LabelLeadingContentUM = LabelLeadingContentUM.None, @DrawableRes val icon: Int? = null, val onIconClick: (() -> Unit)? = null, val onClick: (() -> Unit)? = null, ) +@Immutable +sealed class LabelLeadingContentUM { + data object None : LabelLeadingContentUM() + data class Token(val iconUrl: String) : LabelLeadingContentUM() +} + enum class LabelStyle { REGULAR, ACCENT, WARNING, +} + +enum class LabelSize { + REGULAR, BIG, } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt new file mode 100644 index 0000000000..8dc43f2d53 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt @@ -0,0 +1,130 @@ +package com.tangem.core.ui.components.pager + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +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.TangemThemePreview + +/** + * Horizontal pager indicator + * + * @param pagerState state of pager + * @param indicatorCount counter of visible indicator items + */ +@Composable +fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier, indicatorCount: Int = 5) { + val listState = rememberLazyListState() + + val indicatorColor = TangemTheme.colors.control.key + val overlayColor = TangemTheme.colors.overlay.secondary + val indicatorSize = 8.dp + val spacing = 4.dp + + val totalWidth: Dp = indicatorSize * indicatorCount + spacing * (indicatorCount - 1) + val widthInPx = LocalDensity.current.run { indicatorSize.toPx() } + + val currentItem by remember { + derivedStateOf { + pagerState.currentPage + } + } + + val itemCount = pagerState.pageCount + + LaunchedEffect(key1 = currentItem) { + val viewportSize = listState.layoutInfo.viewportSize + listState.animateScrollToItem( + currentItem, + (widthInPx / 2 - viewportSize.width / 2).toInt(), + ) + } + + Box( + modifier = modifier + .height(32.dp) + .background( + color = overlayColor, + shape = CircleShape, + ) + .padding(horizontal = 16.dp, vertical = 12.dp), + contentAlignment = Alignment.Center, + ) { + LazyRow( + modifier = Modifier + .width(totalWidth), + state = listState, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + userScrollEnabled = false, + ) { + indicatorItems( + itemCount = itemCount, + currentItem = currentItem, + indicatorShape = CircleShape, + activeColor = indicatorColor, + inActiveColor = indicatorColor.copy(alpha = 0.5f), + indicatorSize = indicatorSize, + ) + } + } +} + +@Suppress("LongParameterList") +private fun LazyListScope.indicatorItems( + itemCount: Int, + currentItem: Int, + indicatorShape: Shape, + activeColor: Color, + inActiveColor: Color, + indicatorSize: Dp, +) { + items(itemCount) { index -> + + val isSelected = index == currentItem + + Box( + modifier = Modifier + .clip(indicatorShape) + .size(indicatorSize) + .background( + if (isSelected) activeColor else inActiveColor, + indicatorShape, + ), + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicatorPreviewFirstPage() { + TangemThemePreview { + Box( + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .padding(), + contentAlignment = Alignment.Center, + ) { + val pagerState = rememberPagerState( + initialPage = 0, + pageCount = { 10 }, + ) + PagerIndicator(pagerState = pagerState) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt index 173b9d2094..3da6b245e1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt @@ -246,7 +246,8 @@ data class EventMessageAction( * * @param onClick The action to perform when the button is clicked. By default, it dismisses the message. * */ - fun cancelAction(onClick: () -> Unit = onDismissRequest) = EventMessageAction( + fun cancelAction(isWarning: Boolean = false, onClick: () -> Unit = onDismissRequest) = EventMessageAction( + isWarning = isWarning, title = resourceReference(id = R.string.common_cancel), onClick = onClick, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt index 08fb5a9e46..dec4c24085 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt @@ -39,6 +39,30 @@ object Dialogs { ) } + /** + * Hot wallet creation not supported dialog + * + * @param leastSupportedVersion least supported OS version name ex. "Android 10" + * @param onDismiss lambda be invoked when dialog is dismissed + */ + fun hotWalletCreationNotSupportedDialog(leastSupportedVersion: String, onDismiss: () -> Unit = {}): DialogMessage { + return DialogMessage( + title = resourceReference( + id = R.string.mobile_wallet_requires_min_os_warning_title, + formatArgs = wrappedList(leastSupportedVersion), + ), + message = resourceReference( + id = R.string.mobile_wallet_requires_min_os_warning_body, + formatArgs = wrappedList(leastSupportedVersion), + ), + firstAction = EventMessageAction( + title = resourceReference(R.string.common_got_it), + onClick = {}, + ), + onDismissRequest = onDismiss, + ) + } + /** * Universal error dialog */ diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/DetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/DetailsScreenTestTags.kt index 33f7b56ef3..bbbeb91dd5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/DetailsScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/DetailsScreenTestTags.kt @@ -3,4 +3,5 @@ package com.tangem.core.ui.test object DetailsScreenTestTags { const val SCREEN_CONTAINER = "DETAILS_SCREEN_CONTAINER" const val SCREEN_ITEM = "DETAILS_SCREEN_ITEM" + const val VERSION_NAME = "DETAILS_SCREEN_VERSION_NAME" } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SendScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SendScreenTestTags.kt index 49103a185d..928a782a86 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/SendScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SendScreenTestTags.kt @@ -5,6 +5,7 @@ object SendScreenTestTags { const val AMOUNT_CONTAINER_TITLE = "SEND_SCREEN_AMOUNT_CONTAINER_TITLE" const val INPUT_TEXT_FIELD = "SEND_SCREEN_INPUT_TEXT_FIELD" + const val AMOUNT_ERROR_TEXT = "SEND_SCREEN_AMOUNT_ERROR_TEXT" const val EQUIVALENT_INPUT_AMOUNT = "SEND_SCREEN_EQUIVALENT_INPUT_AMOUNT" const val EXCHANGE_ICON = "SEND_SCREEN_EXCHANGE_ICON" const val TOKEN_NAME = "SEND_SCREEN_TOKEN_NAME" diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt index 85d0294996..8a5cae1ae4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt @@ -1,6 +1,10 @@ package com.tangem.core.ui.utils import android.text.format.DateFormat +import com.tangem.core.ui.utils.DateTimeFormatters.dateDDMMYYYY +import com.tangem.core.ui.utils.DateTimeFormatters.dateMMMdd +import com.tangem.core.ui.utils.DateTimeFormatters.dateTimeFormatter +import com.tangem.core.ui.utils.DateTimeFormatters.dateYYYY import org.joda.time.DateTime import org.joda.time.format.DateTimeFormat import org.joda.time.format.DateTimeFormatter @@ -80,6 +84,13 @@ object DateTimeFormatters { getBestFormatterBySkeleton("yyyy") } + /** + * Example: "June 31" + */ + val dateDMMM: DateTimeFormatter by lazy { + getBestFormatterBySkeleton("d MMMM") + } + /** * Example: "31.06.2020 12:00", "06/31/2020 12:00", "06/31/2020 12:00 PM" */ diff --git a/core/ui/src/main/res/drawable/ic_explore_16.xml b/core/ui/src/main/res/drawable/ic_explore_16.xml new file mode 100644 index 0000000000..a739a875d4 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_explore_16.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_heart_20.xml b/core/ui/src/main/res/drawable/ic_heart_20.xml new file mode 100644 index 0000000000..6e0f036179 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_heart_20.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_quick_recap_16.xml b/core/ui/src/main/res/drawable/ic_quick_recap_16.xml new file mode 100644 index 0000000000..11a259eed3 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_quick_recap_16.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/core/utils/detekt-baseline-main.xml b/core/utils/detekt-baseline-main.xml new file mode 100644 index 0000000000..82dd627f26 --- /dev/null +++ b/core/utils/detekt-baseline-main.xml @@ -0,0 +1,14 @@ + + + + + BooleanPropertyNaming:Retryer.kt$Retryer$val result = try { block(iteration) } catch (e: CancellationException) { throw e } catch (e: Error) { throw e } catch (_: Exception) { false } + MultilineLambdaItParameter:Converter.kt$Converter${ try { convert(it) } catch (throwable: Throwable) { onError?.invoke(throwable) null } } + MultilineLambdaItParameter:PeriodicTask.kt$PeriodicTask${ if (!isActive.get()) { return@onFailure } onError.invoke(it) } + MultilineLambdaItParameter:PeriodicTask.kt$PeriodicTask${ if (!isActive.get()) { return@onSuccess } onSuccess.invoke(it) } + NullableBooleanCheck:JobHolder.kt$JobHolder$job?.isActive ?: false + PropertyUsedBeforeDeclaration:JobHolder.kt$JobHolder$job + SuspendFunSwallowedCancellation:CoroutineExt.kt$runCatching + VarCouldBeVal:PeriodicTask.kt$PeriodicTask$private var isActive: AtomicBoolean = AtomicBoolean(false) + + diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt index 2da5ac22bc..dc8ddf5dfa 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt @@ -36,6 +36,7 @@ internal class AccountListConverter @AssistedInject constructor( userWalletId = userWallet.walletId, accounts = value.accounts.map(cryptoPortfolioConverter::convert), totalAccounts = value.wallet.totalAccounts, + totalArchivedAccounts = value.wallet.totalArchivedAccounts, sortType = sortType, groupType = groupType, ) diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt index 3f741ba0b5..72e746c1bc 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt @@ -27,6 +27,7 @@ internal class GetWalletAccountsResponseConverter @AssistedInject constructor( group = TokensGroupTypeConverter.convertBack(value.groupType), sort = TokensSortTypeConverter.convertBack(value.sortType), totalAccounts = value.totalAccounts, + totalArchivedAccounts = value.totalArchivedAccounts, ), accounts = value.accounts .filterIsInstance() diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt index 3bc99dc784..8c475f3b58 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt @@ -94,21 +94,23 @@ internal class DefaultWalletAccountsFetcher @Inject constructor( override suspend fun push( userWalletId: UserWalletId, body: SaveWalletAccountsResponse, + ): GetWalletAccountsResponse? { + return pushInternal(userWalletId = userWalletId, body = body) + } + + private suspend fun pushInternal( + userWalletId: UserWalletId, + body: SaveWalletAccountsResponse, + eTag: String? = null, ): GetWalletAccountsResponse? { return safeApiCall( call = { - var eTag = getETag(userWalletId) - - if (eTag == null) { - fetch(userWalletId) - - eTag = getETag(userWalletId) ?: error("ETag is null after fetch") - } + val resolvedETag = eTag ?: getETagForPush(userWalletId) val apiResponse = withContext(dispatchers.io) { tangemTechApi.saveWalletAccounts( walletId = userWalletId.stringValue, - eTag = eTag, + eTag = resolvedETag, body = body, ) } @@ -127,6 +129,19 @@ internal class DefaultWalletAccountsFetcher @Inject constructor( ) } + private suspend fun getETagForPush(userWalletId: UserWalletId): String { + var savedETag = getETag(userWalletId) + + if (savedETag == null) { + fetch(userWalletId) + + savedETag = getETag(userWalletId) + ?: error("Failed to retrieve ETag after fetching wallet accounts for wallet $userWalletId") + } + + return savedETag + } + private suspend fun fetchWalletAccounts( userWalletId: UserWalletId, savedAccountsResponse: GetWalletAccountsResponse?, @@ -154,7 +169,13 @@ internal class DefaultWalletAccountsFetcher @Inject constructor( error = throwable, userWalletId = userWalletId, savedAccountsResponse = savedAccountsResponse, - pushWalletAccounts = ::push, + pushWalletAccounts = { accounts, eTag -> + pushInternal( + userWalletId = userWalletId, + body = SaveWalletAccountsResponse(accounts), + eTag = eTag, + ) + }, storeWalletAccounts = ::store, ) }, diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt index 9e86b4848b..772a1dce68 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt @@ -2,7 +2,6 @@ package com.tangem.data.account.fetcher import com.tangem.data.account.fetcher.DefaultWalletAccountsFetcher.FetchResult import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory -import com.tangem.data.common.cache.etag.ETagsStore import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher import com.tangem.data.common.currency.UserTokensSaver import com.tangem.datasource.api.common.response.ApiResponse @@ -29,10 +28,10 @@ import javax.inject.Singleton * Handles errors that occur during the fetching of wallet accounts * * @property tangemTechApi API for network requests + * @property userWalletsStore provides access to user wallets storage * @property userTokensSaver saves user tokens to the storage * @property userTokensResponseStore provides access to user token responses. * @property defaultWalletAccountsResponseFactory creates [GetWalletAccountsResponse] from [UserTokensResponse] - * @property eTagsStore store for ETags to manage caching * @property dispatchers dispatchers * * @see DefaultWalletAccountsFetcher @@ -47,7 +46,6 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor( private val userTokensSaver: UserTokensSaver, private val userTokensResponseStore: UserTokensResponseStore, private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory, - private val eTagsStore: ETagsStore, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -66,7 +64,7 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor( error: ApiResponseError, userWalletId: UserWalletId, savedAccountsResponse: GetWalletAccountsResponse?, - pushWalletAccounts: suspend (UserWalletId, List) -> GetWalletAccountsResponse?, + pushWalletAccounts: suspend (List, String) -> GetWalletAccountsResponse?, storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit, ): FetchResult { val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED) @@ -87,9 +85,7 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor( val eTag = createWallet(userWalletId) if (eTag != null) { - eTagsStore.store(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts, value = eTag) - - pushWalletAccounts(userWalletId, accountDTOs) + pushWalletAccounts(accountDTOs, eTag) userTokensSaver.pushWithRetryer(userWalletId, userTokensResponse) } } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt index 87a090099e..023e9161ec 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt @@ -43,6 +43,7 @@ internal class DefaultWalletAccountsResponseFactory @Inject constructor( group = response.group, sort = response.sort, totalAccounts = accountDTOs.size, + totalArchivedAccounts = 0, ), accounts = accountDTOs.assignTokens(userWalletId = userWalletId, tokens = response.tokens), unassignedTokens = emptyList(), diff --git a/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt b/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt index a62a9840fb..f019c1b5fe 100644 --- a/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt +++ b/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt @@ -53,6 +53,7 @@ internal fun createGetWalletAccountsResponse( group = groupType, sort = sortType, totalAccounts = 1, + totalArchivedAccounts = 0, ), accounts = buildList { createWalletAccountDTO( @@ -79,6 +80,7 @@ internal fun createAccountList( userWalletId = userWalletId, accounts = listOf(createCryptoPortfolio(userWalletId)), totalAccounts = 1, + totalArchivedAccounts = 0, sortType = sortType, groupType = groupType, ) diff --git a/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt index e192321af6..d97ef69569 100644 --- a/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt @@ -137,6 +137,7 @@ class AccountListConverterTest { group = UserTokensResponse.GroupType.NETWORK, sort = UserTokensResponse.SortType.BALANCE, totalAccounts = 1, + totalArchivedAccounts = 0, ), accounts = emptyList(), unassignedTokens = emptyList(), diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt index 3c8951fa2b..51d9c2ad3f 100644 --- a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt @@ -3,7 +3,6 @@ package com.tangem.data.account.fetcher import com.tangem.data.account.converter.createGetWalletAccountsResponse import com.tangem.data.account.converter.createWalletAccountDTO import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory -import com.tangem.data.common.cache.etag.ETagsStore import com.tangem.data.common.currency.UserTokensSaver import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError @@ -38,7 +37,6 @@ class FetchWalletAccountsErrorHandlerTest { private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true) private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true) private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk() - private val eTagsStore: ETagsStore = mockk(relaxUnitFun = true) private val handler = FetchWalletAccountsErrorHandler( tangemTechApi = tangemTechApi, @@ -46,17 +44,18 @@ class FetchWalletAccountsErrorHandlerTest { userTokensSaver = userTokensSaver, userTokensResponseStore = userTokensResponseStore, defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory, - eTagsStore = eTagsStore, dispatchers = TestingCoroutineDispatcherProvider(), ) - private val pushWalletAccounts: suspend (UserWalletId, List) -> GetWalletAccountsResponse = + private val pushWalletAccounts: suspend (List, String) -> GetWalletAccountsResponse = mockk(relaxed = true) private val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true) @BeforeEach fun setupEach() { clearMocks( + tangemTechApi, + userWalletsStore, userTokensSaver, userTokensResponseStore, defaultWalletAccountsResponseFactory, @@ -129,7 +128,7 @@ class FetchWalletAccountsErrorHandlerTest { ), ) } returns apiResponse - coEvery { pushWalletAccounts(userWalletId, listOf(accountDTO)) } returns savedAccountsResponse + coEvery { pushWalletAccounts(listOf(accountDTO), eTagValue) } returns savedAccountsResponse // Act handler.handle( @@ -150,8 +149,7 @@ class FetchWalletAccountsErrorHandlerTest { walletType = WalletType.COLD, ), ) - eTagsStore.store(userWalletId, ETagsStore.Key.WalletAccounts, eTagValue) - pushWalletAccounts(userWalletId, listOf(accountDTO)) + pushWalletAccounts(listOf(accountDTO), eTagValue) storeWalletAccounts(userWalletId, savedAccountsResponse) } @@ -182,6 +180,7 @@ class FetchWalletAccountsErrorHandlerTest { group = UserTokensResponse.GroupType.NONE, sort = UserTokensResponse.SortType.MANUAL, totalAccounts = 1, + totalArchivedAccounts = 0, ), accounts = listOf(accountDTO), unassignedTokens = emptyList(), diff --git a/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt b/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt index fb8f294326..025b7d07bd 100644 --- a/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt @@ -179,6 +179,7 @@ class DefaultMainAccountTokensMigrationTest { group = UserTokensResponse.GroupType.NONE, sort = UserTokensResponse.SortType.MANUAL, totalAccounts = 2, + totalArchivedAccounts = 0, ), accounts = listOf(mainAccount, selectedAccount), unassignedTokens = emptyList(), diff --git a/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt index 674c15d34b..86949ca186 100644 --- a/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt @@ -81,6 +81,7 @@ class DefaultWalletAccountsResponseFactoryTest { group = UserTokensResponse.GroupType.NETWORK, sort = UserTokensResponse.SortType.BALANCE, totalAccounts = 0, + totalArchivedAccounts = 0, ), accounts = emptyList(), unassignedTokens = emptyList(), @@ -135,6 +136,7 @@ class DefaultWalletAccountsResponseFactoryTest { group = defaultResponse.group, sort = defaultResponse.sort, totalAccounts = 1, + totalArchivedAccounts = 0, ), accounts = listOf(accountsDTO.copy(tokens = listOf(token))), unassignedTokens = emptyList(), @@ -190,6 +192,7 @@ class DefaultWalletAccountsResponseFactoryTest { group = defaultResponse.group, sort = defaultResponse.sort, totalAccounts = 0, + totalArchivedAccounts = 0, ), accounts = emptyList(), unassignedTokens = emptyList(), @@ -228,6 +231,7 @@ class DefaultWalletAccountsResponseFactoryTest { group = userTokensResponse.group, sort = userTokensResponse.sort, totalAccounts = 1, + totalArchivedAccounts = 0, ), accounts = listOf(accountsDTO.copy(tokens = userTokensResponse.tokens)), unassignedTokens = emptyList(), diff --git a/data/account/src/test/java/com/tangem/data/account/utils/GetWalletAccountsResponseExtTest.kt b/data/account/src/test/java/com/tangem/data/account/utils/GetWalletAccountsResponseExtTest.kt index 9124a77517..4cea27b28c 100644 --- a/data/account/src/test/java/com/tangem/data/account/utils/GetWalletAccountsResponseExtTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/utils/GetWalletAccountsResponseExtTest.kt @@ -32,6 +32,7 @@ class GetWalletAccountsResponseExtTest { group = UserTokensResponse.GroupType.NONE, sort = UserTokensResponse.SortType.MANUAL, totalAccounts = 0, + totalArchivedAccounts = 0, ), accounts = emptyList(), unassignedTokens = emptyList(), @@ -55,6 +56,7 @@ class GetWalletAccountsResponseExtTest { group = UserTokensResponse.GroupType.NONE, sort = UserTokensResponse.SortType.MANUAL, totalAccounts = 1, + totalArchivedAccounts = 0, ), accounts = listOf(account), unassignedTokens = emptyList(), @@ -84,6 +86,7 @@ class GetWalletAccountsResponseExtTest { group = UserTokensResponse.GroupType.NONE, sort = UserTokensResponse.SortType.MANUAL, totalAccounts = 2, + totalArchivedAccounts = 0, ), accounts = listOf(account1, account2, account3), unassignedTokens = emptyList(), @@ -110,6 +113,7 @@ class GetWalletAccountsResponseExtTest { group = UserTokensResponse.GroupType.NONE, sort = UserTokensResponse.SortType.MANUAL, totalAccounts = 0, + totalArchivedAccounts = 0, ), accounts = emptyList(), unassignedTokens = emptyList(), @@ -141,6 +145,7 @@ class GetWalletAccountsResponseExtTest { group = UserTokensResponse.GroupType.NETWORK, sort = UserTokensResponse.SortType.BALANCE, totalAccounts = 1, + totalArchivedAccounts = 0, ), accounts = listOf(account), unassignedTokens = listOf(token2), @@ -178,6 +183,7 @@ class GetWalletAccountsResponseExtTest { group = UserTokensResponse.GroupType.NONE, sort = UserTokensResponse.SortType.MANUAL, totalAccounts = 2, + totalArchivedAccounts = 0, ), accounts = listOf(account1, account2), unassignedTokens = listOf(token1, token2), @@ -217,6 +223,7 @@ class GetWalletAccountsResponseExtTest { group = UserTokensResponse.GroupType.NONE, sort = UserTokensResponse.SortType.MANUAL, totalAccounts = 1, + totalArchivedAccounts = 0, ), accounts = listOf(account), unassignedTokens = emptyList(), @@ -248,6 +255,7 @@ class GetWalletAccountsResponseExtTest { group = UserTokensResponse.GroupType.NONE, sort = UserTokensResponse.SortType.MANUAL, totalAccounts = 2, + totalArchivedAccounts = 0, ), accounts = listOf(account), unassignedTokens = listOf(token1, token2), diff --git a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt index bcc535bed2..aae64594c5 100644 --- a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt +++ b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/DefaultAppCurrencyRepository.kt @@ -5,12 +5,8 @@ import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.cache.CacheRegistry import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObject -import com.tangem.datasource.local.preferences.utils.getSyncOrNull -import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -24,7 +20,7 @@ import org.joda.time.Duration internal class DefaultAppCurrencyRepository( private val tangemTechApi: TangemTechApi, - private val appPreferencesStore: AppPreferencesStore, + private val appCurrencyResponseStore: AppCurrencyResponseStore, private val availableAppCurrenciesStore: AvailableAppCurrenciesStore, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, @@ -35,15 +31,15 @@ internal class DefaultAppCurrencyRepository( override fun getSelectedAppCurrency(): Flow { return channelFlow { launch { - appPreferencesStore - .getObject(key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY) + appCurrencyResponseStore + .get() .filterNotNull() .map(appCurrencyConverter::convert) .collect(::send) } withContext(dispatchers.io) { - if (appPreferencesStore.getSyncOrNull(PreferencesKeys.SELECTED_APP_CURRENCY_KEY) == null) { + if (appCurrencyResponseStore.getSyncOrNull() == null) { fetchDefaultAppCurrency() } } @@ -70,17 +66,14 @@ internal class DefaultAppCurrencyRepository( "Unable to find app currency with provided code: $currencyCode" } - appPreferencesStore.storeObject( - key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY, - value = currency, - ) + appCurrencyResponseStore.store(currency) } } override suspend fun fetchDefaultAppCurrency(isRefresh: Boolean) { withContext(dispatchers.io) { fetchAvailableCurrenciesIfExpired(isRefresh) - val appCurrency = appPreferencesStore.getSyncOrNull(PreferencesKeys.SELECTED_APP_CURRENCY_KEY) + val appCurrency = appCurrencyResponseStore.getSyncOrNull()?.code changeAppCurrency(appCurrency ?: DEFAULT_CURRENCY_CODE) } } diff --git a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/di/AppCurrencyDataModule.kt b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/di/AppCurrencyDataModule.kt index d9cb9be5f7..719823ed4a 100644 --- a/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/di/AppCurrencyDataModule.kt +++ b/data/app-currency/src/main/kotlin/com/tangem/data/appcurrency/di/AppCurrencyDataModule.kt @@ -3,8 +3,8 @@ package com.tangem.data.appcurrency.di import com.tangem.data.appcurrency.DefaultAppCurrencyRepository import com.tangem.data.common.cache.CacheRegistry import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore -import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -21,17 +21,17 @@ internal object AppCurrencyDataModule { @Singleton fun provideAppCurrencyRepository( tangemTechApi: TangemTechApi, - appPreferencesStore: AppPreferencesStore, + appCurrencyResponseStore: AppCurrencyResponseStore, availableAppCurrenciesStore: AvailableAppCurrenciesStore, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, ): AppCurrencyRepository { return DefaultAppCurrencyRepository( tangemTechApi = tangemTechApi, - appPreferencesStore = appPreferencesStore, availableAppCurrenciesStore = availableAppCurrenciesStore, cacheRegistry = cacheRegistry, dispatchers = dispatchers, + appCurrencyResponseStore = appCurrencyResponseStore, ) } } \ No newline at end of file diff --git a/data/common/build.gradle.kts b/data/common/build.gradle.kts index b418e04abc..cb2baa1194 100644 --- a/data/common/build.gradle.kts +++ b/data/common/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.networks) + implementation(projects.domain.walletManager) implementation(projects.domain.wallets) /* Libs - SDK */ diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricher.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricher.kt index aff3f5c5fa..dfd790c608 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricher.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricher.kt @@ -1,55 +1,45 @@ package com.tangem.data.common.currency +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.address.Address +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.networks.multi.MultiNetworkStatusProducer -import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeoutOrNull import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds class UserTokensResponseAddressesEnricher @Inject constructor( private val walletsRepository: WalletsRepository, private val dispatchers: CoroutineDispatcherProvider, - private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, + private val walletManagersFacade: WalletManagersFacade, ) { suspend operator fun invoke(userWalletId: UserWalletId, response: UserTokensResponse): UserTokensResponse { val isNotificationsEnabled = walletsRepository.isNotificationsEnabled(userWalletId) return withContext(dispatchers.default) { - val networksStatuses = if (isNotificationsEnabled) { - withTimeoutOrNull( - FETCH_TIMEOUT_SECONDS.seconds, - { multiNetworkStatusSupplier.invoke(MultiNetworkStatusProducer.Params(userWalletId)).first() }, - ).orEmpty() + val addressByToken = if (isNotificationsEnabled) { + response.tokens.associateWith { token -> + val blockchain = Blockchain.fromNetworkId(token.networkId) ?: return@associateWith null + + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = token.derivationPath, + ) + + walletManager?.wallet?.addresses?.map(Address::value) + } } else { - emptySet() + emptyMap() } val enrichedTokens = response.tokens.map { token -> if (isNotificationsEnabled) { - val matchingNetwork = networksStatuses.find { status -> - status.network.backendId == token.networkId && - status.network.derivationPath.value == token.derivationPath - } ?: return@map token - - val networkAddress = when (matchingNetwork.value) { - is NetworkStatus.Verified -> (matchingNetwork.value as NetworkStatus.Verified).address - is NetworkStatus.NoAccount -> (matchingNetwork.value as NetworkStatus.NoAccount).address - else -> null - } - - val addresses = networkAddress - ?.availableAddresses - ?.map { it.value } - ?.toList() - .orEmpty() + val addresses = addressByToken[token] ?: return@map token token.copy(addresses = addresses) } else { @@ -60,8 +50,4 @@ class UserTokensResponseAddressesEnricher @Inject constructor( response.copy(tokens = enrichedTokens, notifyStatus = isNotificationsEnabled) } } - - companion object { - private const val FETCH_TIMEOUT_SECONDS = 3 - } } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt index 61f43d9ad7..2b4e65a095 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt @@ -2,12 +2,17 @@ package com.tangem.data.common.currency import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.tokens.UserTokensBackwardCompatibility +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.common.response.isNetworkError import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.WalletType import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.retryer.Retryer @@ -50,24 +55,26 @@ class UserTokensSaver( response: UserTokensResponse, useEnricher: Boolean = true, onFailSend: () -> Unit = {}, - ) { - withContext(dispatchers.default) { - val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) + ) = withContext(dispatchers.io) { + val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) + if (userWallet == null) { + Timber.e("UserWallet with id $userWalletId not found. Cannot push tokens.") + onFailSend() + return@withContext + } + + if (accountsFeatureToggles.isFeatureEnabled) { + val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher) + + pushNew(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend) + } else { val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher).copy( - walletName = userWallet?.name, + walletName = userWallet.name, walletType = WalletType.from(userWallet), ) - safeApiCall( - call = { - withContext(dispatchers.io) { - tangemTechApi.saveUserTokens(userId = userWalletId.stringValue, userTokens = enrichedResponse) - .bind() - } - }, - onError = { onFailSend() }, - ) + pushLegacy(userWalletId = userWalletId, response = enrichedResponse, onFailSend = onFailSend) } } @@ -88,6 +95,39 @@ class UserTokensSaver( ) } + private suspend fun pushLegacy(userWalletId: UserWalletId, response: UserTokensResponse, onFailSend: () -> Unit) { + safeApiCall( + call = { tangemTechApi.saveUserTokens(userId = userWalletId.stringValue, userTokens = response).bind() }, + onError = { onFailSend() }, + ) + } + + private suspend fun pushNew(userWallet: UserWallet, response: UserTokensResponse, onFailSend: () -> Unit) { + safeApiCall( + call = { + val apiResponse = tangemTechApi.saveTokens( + userId = userWallet.walletId.stringValue, + userTokens = response, + ) + + val isWalletNotFound = apiResponse is ApiResponse.Error && + apiResponse.cause.isNetworkError(ApiResponseError.HttpException.Code.NOT_FOUND) + + if (isWalletNotFound) { + tangemTechApi.createWallet(body = WalletIdBodyConverter.convert(userWallet)).bind() + + tangemTechApi.saveTokens( + userId = userWallet.walletId.stringValue, + userTokens = response, + ).bind() + } else { + apiResponse.bind() + } + }, + onError = { onFailSend() }, + ) + } + private fun UserTokensResponse.applyCompatibility(): UserTokensResponse { return userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(userTokensResponse = this) } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt index fb49316219..48751fa834 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt @@ -13,7 +13,7 @@ import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.demo.models.DemoConfig -import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.retryer.RetryerPool @@ -54,12 +54,12 @@ internal object DataCommonModule { @Singleton fun provideUserTokensEncricher( walletsRepository: WalletsRepository, - multiNetworkStatusSupplier: MultiNetworkStatusSupplier, + walletManagersFacade: WalletManagersFacade, dispatchers: CoroutineDispatcherProvider, ): UserTokensResponseAddressesEnricher { return UserTokensResponseAddressesEnricher( walletsRepository = walletsRepository, - multiNetworkStatusSupplier = multiNetworkStatusSupplier, + walletManagersFacade = walletManagersFacade, dispatchers = dispatchers, ) } diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricherTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricherTest.kt index 77b27593e6..7e9f617946 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricherTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensResponseAddressesEnricherTest.kt @@ -1,92 +1,68 @@ package com.tangem.data.common.currency import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.address.Address +import com.tangem.blockchain.common.address.AddressType import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.network.NetworkAddress -import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.clearAllMocks +import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.every import io.mockk.mockk -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Before -import org.junit.Test +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class UserTokensResponseAddressesEnricherTest { - private lateinit var walletsRepository: WalletsRepository - private val dispatchers: CoroutineDispatcherProvider = TestingCoroutineDispatcherProvider() - private lateinit var multiNetworkStatusSupplier: MultiNetworkStatusSupplier - private lateinit var enricher: UserTokensResponseAddressesEnricher + private val walletsRepository: WalletsRepository = mockk() + private val walletManagersFacade: WalletManagersFacade = mockk() + private val enricher: UserTokensResponseAddressesEnricher = UserTokensResponseAddressesEnricher( + walletsRepository = walletsRepository, + walletManagersFacade = walletManagersFacade, + dispatchers = TestingCoroutineDispatcherProvider(), + ) - @Before - fun setup() { - walletsRepository = mockk() - multiNetworkStatusSupplier = mockk() + private val userWalletId = UserWalletId("1234567890abcdef") - enricher = UserTokensResponseAddressesEnricher( - walletsRepository = walletsRepository, - dispatchers = dispatchers, - multiNetworkStatusSupplier = multiNetworkStatusSupplier, - ) - } - - @After + @AfterEach fun tearDown() { - clearAllMocks() - } - - @Test - fun `GIVEN notifications are disabled globally WHEN invoke THEN return original response`() = runTest { - // GIVEN - val userWalletId = UserWalletId("1234567890abcdef") - val token = createToken() - val response = createUserTokensResponse(tokens = listOf(token)) - - // WHEN - val result = enricher(userWalletId, response) - - // THEN - assertThat(result).isEqualTo(response) + clearMocks(walletsRepository, walletManagersFacade) } @Test fun `GIVEN notifications are disabled for wallet WHEN invoke THEN return response with empty addresses`() = runTest { // GIVEN - val userWalletId = UserWalletId("1234567890abcdef") val token = createToken() val response = createUserTokensResponse(tokens = listOf(token)) + val walletManager = mockk { + val wallet = mockk { + every { addresses } returns setOf( + Address(value = "0x12345", type = AddressType.Default), + ) + } + + every { this@mockk.wallet } returns wallet + } + coEvery { walletsRepository.isNotificationsEnabled(userWalletId) } returns false + coEvery { - multiNetworkStatusSupplier.invoke(any()) - } returns flowOf( - setOf( - NetworkStatus( - network = mockk { - every { backendId } returns "ethereum" - every { derivationPath.value } returns "m/44'/60'/0'/0/0" - }, - value = NetworkStatus.Verified( - address = mockk { - every { availableAddresses } returns emptySet() - }, - amounts = emptyMap(), - pendingTransactions = emptyMap(), - yieldSupplyStatuses = emptyMap(), - source = StatusSource.ACTUAL, - ), - ), - ), - ) + walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = Blockchain.Ethereum, + derivationPath = token.derivationPath, + ) + } returns walletManager // WHEN val result = enricher(userWalletId, response) @@ -100,75 +76,52 @@ class UserTokensResponseAddressesEnricherTest { fun `GIVEN notifications are enabled and addresses available WHEN invoke THEN return enriched response`() = runTest { // GIVEN - val userWalletId = UserWalletId("1234567890abcdef") val token = createToken() val response = createUserTokensResponse(tokens = listOf(token)) - val addresses = listOf("0x123", "0x456") + val addresses = setOf( + Address(value = "0x123", type = AddressType.Default), + Address(value = "0x456", type = AddressType.Legacy), + ) + + val walletManager = mockk { + val wallet = mockk { + every { this@mockk.addresses } returns addresses + } + + every { this@mockk.wallet } returns wallet + } coEvery { walletsRepository.isNotificationsEnabled(userWalletId) } returns true coEvery { - multiNetworkStatusSupplier.invoke(any()) - } returns flowOf( - setOf( - NetworkStatus( - network = mockk { - every { backendId } returns "ethereum" - every { derivationPath.value } returns "m/44'/60'/0'/0/0" - }, - value = NetworkStatus.Verified( - address = mockk { - every { availableAddresses } returns addresses.map { address -> - mockk { - every { value } returns address - } - }.toSet() - }, - amounts = emptyMap(), - pendingTransactions = emptyMap(), - yieldSupplyStatuses = emptyMap(), - source = StatusSource.ACTUAL, - ), - ), - ), - ) + walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = Blockchain.Ethereum, + derivationPath = token.derivationPath, + ) + } returns walletManager // WHEN val result = enricher(userWalletId, response) // THEN assertThat(result.tokens).hasSize(1) - assertThat(result.tokens[0].addresses).containsExactlyElementsIn(addresses) + assertThat(result.tokens[0].addresses).containsExactlyElementsIn(addresses.map { it.value }) } @Test fun `GIVEN notifications are enabled but no matching network WHEN invoke THEN return original token`() = runTest { // GIVEN - val userWalletId = UserWalletId("1234567890abcdef") val token = createToken() val response = createUserTokensResponse(tokens = listOf(token)) coEvery { walletsRepository.isNotificationsEnabled(userWalletId) } returns true coEvery { - multiNetworkStatusSupplier.invoke(any()) - } returns flowOf( - setOf( - NetworkStatus( - network = mockk { - every { backendId } returns "bitcoin" - every { derivationPath.value } returns "m/44'/0'/0'/0/0" - }, - value = NetworkStatus.Verified( - address = mockk { - every { availableAddresses } returns emptySet() - }, - amounts = emptyMap(), - pendingTransactions = emptyMap(), - yieldSupplyStatuses = emptyMap(), - source = StatusSource.ACTUAL, - ), - ), - ), - ) + walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = Blockchain.Ethereum, + derivationPath = token.derivationPath, + ) + } returns null // WHEN val result = enricher(userWalletId, response) diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt index 24c2d207e7..bc51068bff 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt @@ -73,7 +73,7 @@ class UserTokensSaverTest { } coVerify(inverse = true) { - tangemTechApi.saveUserTokens(any(), any()) + tangemTechApi.saveTokens(any(), any()) } } @@ -108,7 +108,8 @@ class UserTokensSaverTest { coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet coEvery { enricher(userWalletId, response) } returns enrichedResponse - coEvery { tangemTechApi.saveUserTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse + coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse + coEvery { tangemTechApi.createWallet(body = any()) } returns ApiResponse.Error(error) as ApiResponse // WHEN userTokensSaver.push( @@ -120,7 +121,7 @@ class UserTokensSaverTest { // THEN coVerifyOrder { enricher(userWalletId, response) - tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse) + tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse) } assert(onFailSendCalled) { "onFailSend callback should be called when API call fails" } @@ -155,7 +156,7 @@ class UserTokensSaverTest { coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet coEvery { enricher(userWalletId, response) } returns enrichedResponse coEvery { - tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse) + tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse) } returns ApiResponse.Success(Unit) // WHEN @@ -164,7 +165,7 @@ class UserTokensSaverTest { // THEN coVerifyOrder { enricher(userWalletId, response) - tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse) + tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse) } } } \ No newline at end of file diff --git a/data/feedback/detekt-baseline-debug.xml b/data/feedback/detekt-baseline-debug.xml deleted file mode 100644 index d10388bff4..0000000000 --- a/data/feedback/detekt-baseline-debug.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - BooleanPropertyNaming:DefaultFeedbackRepository.kt$DefaultFeedbackRepository$private val useNewUserWalletsRepository: Boolean - - diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt index dcadace780..24ce5d7b07 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -26,7 +26,7 @@ import java.io.File * * @property appLogsStore app logs store * @property userWalletsListManager user wallets list manager - * @property useNewUserWalletsRepository flag to use new user wallets repository + * @property shouldUseNewUserWalletsRepository flag to use new user wallets repository * @property userWalletsListRepository user wallets repository * @property walletManagersStore wallet managers store * @property emailSender email sender @@ -37,7 +37,7 @@ import java.io.File @Suppress("LongParameterList") internal class DefaultFeedbackRepository( private val appLogsStore: AppLogsStore, - private val useNewUserWalletsRepository: Boolean, + private val shouldUseNewUserWalletsRepository: Boolean, private val userWalletsListRepository: UserWalletsListRepository, private val userWalletsListManager: UserWalletsListManager, private val walletManagersStore: WalletManagersStore, @@ -126,7 +126,7 @@ internal class DefaultFeedbackRepository( } private suspend fun getUserWalletById(userWalletId: UserWalletId): UserWallet? { - return if (useNewUserWalletsRepository) { + return if (shouldUseNewUserWalletsRepository) { userWalletsListRepository.userWalletsSync().find { it.walletId == userWalletId } } else { userWalletsListManager.userWalletsSync.find { it.walletId == userWalletId } @@ -134,7 +134,7 @@ internal class DefaultFeedbackRepository( } private fun totalUserWallets(): Int { - return if (useNewUserWalletsRepository) { + return if (shouldUseNewUserWalletsRepository) { userWalletsListRepository.userWallets.value?.size ?: 0 } else { userWalletsListManager.walletsCount diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt index 63f1b43180..d6ecc8ef1e 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt @@ -42,7 +42,7 @@ internal object FeedbackModule { emailSender = emailSender, appVersionProvider = appVersionProvider, userWalletsListRepository = userWalletsListRepository, - useNewUserWalletsRepository = hotWalletFeatureToggles.isHotWalletEnabled, + shouldUseNewUserWalletsRepository = hotWalletFeatureToggles.isHotWalletEnabled, getSelectedWalletUseCase = getSelectedWalletUseCase, ) } diff --git a/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt b/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt index 4670884954..5754358efa 100644 --- a/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt +++ b/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt @@ -1,5 +1,7 @@ package com.tangem.data.hotwallet +import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectMap @@ -12,6 +14,13 @@ internal class DefaultHotWalletRepository( private val appPreferencesStore: AppPreferencesStore, ) : HotWalletRepository { + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q) + override fun isWalletCreationSupported(): Boolean { + return BuildConfig.DEBUG || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + } + + override fun getLeastSupportedAndroidVersionName(): String = "Android 10" + override fun accessCodeSkipped(userWalletId: UserWalletId): Flow = appPreferencesStore .getObjectMap(PreferencesKeys.ACCESS_CODE_SKIPPED_STATES_KEY) .map { it[userWalletId.stringValue] == true } diff --git a/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt b/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt index e42068534e..426bc6f287 100644 --- a/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt +++ b/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt @@ -18,7 +18,6 @@ import com.tangem.pagination.fetcher.BatchFetcher import com.tangem.pagination.toBatchFlow import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching -import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -31,7 +30,7 @@ import kotlin.collections.orEmpty * Implementation of [NewsRepository]. [REDACTED_AUTHOR] */ -internal class DefaultNewsRepository @Inject constructor( +internal class DefaultNewsRepository( private val newsApi: NewsApi, private val dispatchers: CoroutineDispatcherProvider, private val newsDetailsStore: NewsDetailsStore, @@ -68,8 +67,8 @@ internal class DefaultNewsRepository @Inject constructor( fetchDetailedArticlesInternal(newsIds = newsIds, language = language) } - override suspend fun getTrendingNews(limit: Int, language: String?): List { - return fetchAndStoreTrendingNews(limit = limit, language = language) + override suspend fun getTrendingNews(limit: Int, language: String?) { + fetchAndStoreTrendingNews(limit = limit, language = language) } override fun observeTrendingNews(): Flow> { diff --git a/data/nft/build.gradle.kts b/data/nft/build.gradle.kts index 64d14695e9..4124798b29 100644 --- a/data/nft/build.gradle.kts +++ b/data/nft/build.gradle.kts @@ -12,6 +12,10 @@ android { namespace = "com.tangem.data.nft" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { /** Project - Data */ @@ -53,4 +57,8 @@ dependencies { /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + + testImplementation(projects.test.core) + testImplementation(projects.common.test) + testRuntimeOnly(deps.test.junit5.engine) } \ No newline at end of file diff --git a/data/nft/detekt-baseline-debug.xml b/data/nft/detekt-baseline-debug.xml index 08a3e3aba8..d4e81aea8d 100644 --- a/data/nft/detekt-baseline-debug.xml +++ b/data/nft/detekt-baseline-debug.xml @@ -3,7 +3,6 @@ MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ NFTCollections( network = network, content = NFTCollections.Content.Collections( collections = it ?.map { collection -> nftSdkCollectionConverter.convert(network to collection) } ?.filter { it.id !is NFTCollection.Identifier.Unknown }, source = StatusSource.CACHE, ), ) } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ getNFTPersistenceStore(userWalletId, it).clear() getNFTRuntimeStore(userWalletId, it).clear() } MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ if (it !is UnsupportedOperationException) { saveFailedStateInRuntime( userWalletId = userWalletId, network = network, error = it, ) } } MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ if (it.id == collectionId) { it.changeAssetsStatusSource(source) } else { it } } MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ if (it.identifier == sdkCollectionId) { it.copy(assets = assets) } else { it } } diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index 7ceb082dd5..af884a8963 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -1,5 +1,6 @@ package com.tangem.data.nft +import android.content.Context import android.content.res.Resources import arrow.core.Either import com.tangem.blockchain.common.Blockchain @@ -26,15 +27,19 @@ import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.nft.models.NFTCollections import com.tangem.domain.nft.models.NFTSalePrice import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.nft.utils.NFTCleaner import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.coroutines.saveIn +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import timber.log.Timber import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset @@ -49,9 +54,10 @@ internal class DefaultNFTRepository @Inject constructor( private val userWalletsStore: UserWalletsStore, private val networkFactory: NetworkFactory, private val excludedBlockchains: ExcludedBlockchains, - resources: Resources, -) : NFTRepository { + @ApplicationContext private val context: Context, +) : NFTRepository, NFTCleaner { + private val resources: Resources by lazy { context.resources } private val networkJobs = ConcurrentHashMap() private val collectionJobs = ConcurrentHashMap() private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains) @@ -218,10 +224,22 @@ internal class DefaultNFTRepository @Inject constructor( assetIdentifier = assetIdConverter.convertBack(assetIdentifier), ) - override suspend fun clearCache(userWalletId: UserWalletId, networks: List) { - networks.forEach { - getNFTPersistenceStore(userWalletId, it).clear() - getNFTRuntimeStore(userWalletId, it).clear() + // NFTCleaner implementation + override suspend fun invoke(userWalletId: UserWalletId, networks: Set) { + if (networks.isEmpty()) { + Timber.d("No networks to clear for wallet: $userWalletId") + return + } + + networks.forEach { network -> + runSuspendCatching { + getNFTPersistenceStore(userWalletId = userWalletId, network = network).clear() + // FIXME: nftRuntimeStore is created with only network, so clearing it may affect other wallets + // nftRuntimeStoreFactory.provide(network = network).clear() + } + .onFailure { throwable -> + Timber.e(throwable, "Failed to clear NFT data for network $network for wallet: $userWalletId") + } } } diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/di/NFTDataModule.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/di/NFTDataModule.kt index beeca8c0c2..d5fd54fa62 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/di/NFTDataModule.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/di/NFTDataModule.kt @@ -1,45 +1,23 @@ package com.tangem.data.nft.di -import android.content.Context -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.data.common.network.NetworkFactory import com.tangem.data.nft.DefaultNFTRepository -import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory -import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.nft.repository.NFTRepository -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.domain.nft.utils.NFTCleaner +import dagger.Binds import dagger.Module -import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object NFTDataModule { +internal interface NFTDataModule { - @Provides + @Binds @Singleton - fun provideNFTRepository( - @ApplicationContext context: Context, - nftPersistenceStoreFactory: NFTPersistenceStoreFactory, - nftRuntimeStoreFactory: NFTRuntimeStoreFactory, - walletManagersFacade: WalletManagersFacade, - dispatchers: CoroutineDispatcherProvider, - excludedBlockchains: ExcludedBlockchains, - userWalletsStore: UserWalletsStore, - networkFactory: NetworkFactory, - ): NFTRepository = DefaultNFTRepository( - nftPersistenceStoreFactory = nftPersistenceStoreFactory, - nftRuntimeStoreFactory = nftRuntimeStoreFactory, - walletManagersFacade = walletManagersFacade, - dispatchers = dispatchers, - excludedBlockchains = excludedBlockchains, - userWalletsStore = userWalletsStore, - networkFactory = networkFactory, - resources = context.resources, - ) + fun bindNFTRepository(defaultNFTRepository: DefaultNFTRepository): NFTRepository + + @Binds + @Singleton + fun bindNFTCleaner(defaultNFTRepository: DefaultNFTRepository): NFTCleaner } \ No newline at end of file diff --git a/data/nft/src/test/kotlin/com/tangem/data/nft/NFTCleanerTest.kt b/data/nft/src/test/kotlin/com/tangem/data/nft/NFTCleanerTest.kt new file mode 100644 index 0000000000..1a27526992 --- /dev/null +++ b/data/nft/src/test/kotlin/com/tangem/data/nft/NFTCleanerTest.kt @@ -0,0 +1,78 @@ +package com.tangem.data.nft + +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.datasource.local.nft.NFTPersistenceStore +import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory +import com.tangem.datasource.local.nft.NFTRuntimeStore +import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class NFTCleanerTest { + + private val nftPersistenceStoreFactory: NFTPersistenceStoreFactory = mockk() + private val nftRuntimeStoreFactory: NFTRuntimeStoreFactory = mockk() + + private val nftCleaner = DefaultNFTRepository( + nftPersistenceStoreFactory = nftPersistenceStoreFactory, + nftRuntimeStoreFactory = nftRuntimeStoreFactory, + walletManagersFacade = mockk(), + dispatchers = mockk(), + userWalletsStore = mockk(), + networkFactory = mockk(), + excludedBlockchains = mockk(), + context = mockk(), + ) + + private val userWalletId = UserWalletId("011") + + @AfterEach + fun tearDown() { + clearMocks(nftPersistenceStoreFactory, nftRuntimeStoreFactory) + } + + @Test + fun `should call invoke with multiple networks`() = runTest { + // Arrange + val mockCryptoCurrencyFactory = MockCryptoCurrencyFactory() + val networks = mockCryptoCurrencyFactory.ethereumAndStellar.map(CryptoCurrency.Coin::network) + val persistenceByNetwork = networks.associateWith { mockk(relaxUnitFun = true) } + val runtimeByNetwork = networks.associateWith { mockk(relaxUnitFun = true) } + + networks.forEach { network -> + every { nftPersistenceStoreFactory.provide(userWalletId, network) } returns persistenceByNetwork[network]!! + every { nftRuntimeStoreFactory.provide(network) } returns runtimeByNetwork[network]!! + } + + // Act + nftCleaner.invoke(userWalletId = userWalletId, networks = networks.toSet()) + + // Assert + coVerifyOrder { + networks.forEach { network -> + nftPersistenceStoreFactory.provide(userWalletId, network) + persistenceByNetwork[network]!!.clear() + // nftRuntimeStoreFactory.provide(network) + // runtimeByNetwork[network]!!.clear() + } + } + } + + @Test + fun `should handle empty networks set`() = runTest { + // Act + nftCleaner.invoke(userWalletId, emptySet()) + + // Assert + coVerify(inverse = true) { + nftPersistenceStoreFactory.provide(userWalletId = any(), network = any()) + nftRuntimeStoreFactory.provide(network = any()) + } + } +} \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt index cfffffb159..b1552db7cc 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt @@ -11,13 +11,10 @@ import com.tangem.data.onramp.converters.HotCryptoCurrencyConverter import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObject import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.common.extensions.canHandleBlockchain @@ -39,13 +36,13 @@ import timber.log.Timber /** * Default implementation of [HotCryptoRepository] * - * @property excludedBlockchains excluded blockchains - * @property hotCryptoResponseStore store of `HotCryptoResponse` - * @property userWalletsStore store of `UserWallet` - * @property tangemTechApi tangem tech api - * @property appPreferencesStore app preferences store - * @property dispatchers dispatchers - * @property analyticsEventHandler analytics event handler + * @property excludedBlockchains excluded blockchains + * @property hotCryptoResponseStore store of `HotCryptoResponse` + * @property userWalletsStore store of `UserWallet` + * @property tangemTechApi tangem tech api + * @property appCurrencyResponseStore store of current app currency + * @property dispatchers dispatchers + * @property analyticsEventHandler analytics event handler * [REDACTED_AUTHOR] */ @@ -56,7 +53,7 @@ internal class DefaultHotCryptoRepository( private val hotCryptoResponseStore: HotCryptoResponseStore, private val userWalletsStore: UserWalletsStore, private val tangemTechApi: TangemTechApi, - private val appPreferencesStore: AppPreferencesStore, + private val appCurrencyResponseStore: AppCurrencyResponseStore, private val userTokensResponseStore: UserTokensResponseStore, private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, @@ -112,8 +109,8 @@ internal class DefaultHotCryptoRepository( } private fun getHotCryptoFlow(): Flow { - return appPreferencesStore - .getObject(key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY) + return appCurrencyResponseStore + .get() .map { it?.id ?: "usd" } .distinctUntilChanged() .map { getHotCrypto(appCurrencyId = it).getOrNull() } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt index b953fd69c7..2e80138833 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt @@ -13,6 +13,7 @@ import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.api.onramp.OnrampApi import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore @@ -104,7 +105,7 @@ internal object OnrampDataModule { hotCryptoResponseStore: HotCryptoResponseStore, userWalletsStore: UserWalletsStore, tangemTechApi: TangemTechApi, - appPreferencesStore: AppPreferencesStore, + appCurrencyResponseStore: AppCurrencyResponseStore, dispatchers: CoroutineDispatcherProvider, analyticsEventHandler: AnalyticsEventHandler, userTokensResponseStore: UserTokensResponseStore, @@ -114,7 +115,7 @@ internal object OnrampDataModule { hotCryptoResponseStore = hotCryptoResponseStore, userWalletsStore = userWalletsStore, tangemTechApi = tangemTechApi, - appPreferencesStore = appPreferencesStore, + appCurrencyResponseStore = appCurrencyResponseStore, dispatchers = dispatchers, analyticsEventHandler = analyticsEventHandler, userTokensResponseStore = userTokensResponseStore, diff --git a/data/promo/detekt-baseline-debug.xml b/data/promo/detekt-baseline-debug.xml deleted file mode 100644 index 7ac3d42cb4..0000000000 --- a/data/promo/detekt-baseline-debug.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - SuspendFunSwallowedCancellation:DefaultPromoRepository.kt$DefaultPromoRepository$runCatching - SuspendFunWithFlowReturnType:DefaultPromoRepository.kt$DefaultPromoRepository$suspend - - diff --git a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt index d6041cb705..14a73f37dd 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt @@ -10,6 +10,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.getShouldShowStor import com.tangem.datasource.local.preferences.utils.get import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.store +import com.tangem.datasource.local.promo.PromoBannerStore import com.tangem.datasource.local.promo.PromoStoriesStore import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.promo.PromoRepository @@ -19,6 +20,7 @@ import com.tangem.domain.promo.models.StoryContent import com.tangem.feature.referral.domain.ReferralRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching +import com.tangem.utils.coroutines.runSuspendCatching import kotlinx.coroutines.flow.* import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull @@ -27,6 +29,7 @@ internal class DefaultPromoRepository( private val tangemApi: TangemTechApi, private val appPreferencesStore: AppPreferencesStore, private val promoStoriesStore: PromoStoriesStore, + private val promoBannerStore: PromoBannerStore, private val dispatchers: CoroutineDispatcherProvider, private val referralRepository: ReferralRepository, ) : PromoRepository { @@ -42,7 +45,7 @@ internal class DefaultPromoRepository( .distinctUntilChanged() .map { shouldShow -> when (promoId) { - PromoId.Referral -> runCatching { + PromoId.Referral -> runSuspendCatching { !referralRepository.isReferralParticipant(userWalletId) && shouldShow }.getOrDefault(false) PromoId.Sepa -> { @@ -87,7 +90,7 @@ internal class DefaultPromoRepository( appPreferencesStore.store(PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name), false) } - override suspend fun isMarketsStakingNotificationHideClicked(): Flow { + override fun isMarketsStakingNotificationHideClicked(): Flow { return appPreferencesStore.get( key = PreferencesKeys.MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY, default = false, @@ -101,6 +104,18 @@ internal class DefaultPromoRepository( ) } + override suspend fun isMoonpayPromoActive(): Boolean { + val banner = runCatching(dispatchers.io) { + val response = promoBannerStore.getSyncOrNull(MOONPAY_NAME) ?: run { + val apiResponse = tangemApi.getPromoBanner(MOONPAY_NAME).getOrThrow() + promoBannerStore.store(MOONPAY_NAME, apiResponse) + apiResponse + } + promoBannerConverter.convert(response) + }.getOrNull() + return banner?.isActive == true + } + override fun getStoryById(id: String): Flow = isReadyToShowStories(id).mapLatest { getStoryByIdSync(id = id, refresh = false) } @@ -111,7 +126,7 @@ internal class DefaultPromoRepository( val storedPromo = promoStoriesStore.getSyncOrNull(storyId = id) // Get last stored promo by id if possible or get from network val story = if (storedPromo == null && refresh) { - val storyContent = runCatching { + val storyContent = runSuspendCatching { // Important to return withTimeoutOrNull(STORIES_LOAD_DELAY) { tangemApi.getStoryById(storyId = id).getOrThrow() @@ -179,6 +194,7 @@ internal class DefaultPromoRepository( const val SEPA_NAME = "sepa" const val VISA_NAME = "visa-waitlist" const val BLACK_FRIDAY_NAME = "black-friday" + const val MOONPAY_NAME = "moonpay" const val ONE_PLUS_ONE_NAME = "one-plus-one" const val STORIES_LOAD_DELAY = 1000L } diff --git a/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt b/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt index 69d489a2dc..2f0c689e53 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt @@ -3,6 +3,7 @@ package com.tangem.data.promo.di import com.tangem.data.promo.DefaultPromoRepository import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.promo.PromoBannerStore import com.tangem.datasource.local.promo.PromoStoriesStore import com.tangem.domain.promo.PromoRepository import com.tangem.feature.referral.domain.ReferralRepository @@ -23,6 +24,7 @@ internal object PromoDataModule { tangemTechApi: TangemTechApi, appPreferencesStore: AppPreferencesStore, promoStoriesStore: PromoStoriesStore, + promoBannerStore: PromoBannerStore, dispatchers: CoroutineDispatcherProvider, referralRepository: ReferralRepository, ): PromoRepository { @@ -32,6 +34,7 @@ internal object PromoDataModule { promoStoriesStore = promoStoriesStore, dispatchers = dispatchers, referralRepository = referralRepository, + promoBannerStore = promoBannerStore, ) } } \ No newline at end of file diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index a01398d354..fc7059b626 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(deps.androidx.datastore) implementation(deps.jodatime) implementation(deps.kotlin.coroutines) + implementation(deps.kotlin.datetime) implementation(deps.kotlin.immutable.collections) implementation(deps.moshi) implementation(deps.moshi.kotlin) diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt index b97325f373..85f3038652 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt @@ -223,30 +223,18 @@ internal class DefaultP2PEthPoolRepository( .map { vaults -> if (vaults.isEmpty()) { return@map StakingAvailability.TemporaryUnavailable - } - - val vault = findPublicVault(vaults = vaults) - - if (vault != null) { - StakingAvailability.Available(StakingOption.P2P(vault)) } else { - StakingAvailability.TemporaryUnavailable + StakingAvailability.Available(StakingOption.P2P(vaults)) } } } override suspend fun getStakingAvailabilitySync(): StakingAvailability { val vaults = getVaultsSync() - if (vaults.isEmpty()) { - return StakingAvailability.TemporaryUnavailable - } - - val vault = findPublicVault(vaults = vaults) - - return if (vault != null) { - StakingAvailability.Available(StakingOption.P2P(vault)) - } else { + return if (vaults.isEmpty()) { StakingAvailability.TemporaryUnavailable + } else { + StakingAvailability.Available(StakingOption.P2P(vaults)) } } @@ -257,8 +245,4 @@ internal class DefaultP2PEthPoolRepository( private fun getVaultsFlow(): Flow> { return p2pEthPoolVaultsStore.get() } - - private fun findPublicVault(vaults: List): P2PEthPoolVault? { - return vaults.firstOrNull { vault -> !vault.isPrivate } - } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index ee0a73cf7b..2d73b39384 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -3,11 +3,11 @@ package com.tangem.data.staking import arrow.core.getOrElse import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toBlockchain -import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.store.StakingBalancesStore import com.tangem.domain.card.common.TapWorkarounds.isWallet2 import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingAvailability @@ -30,7 +30,7 @@ import kotlinx.coroutines.withContext internal class DefaultStakingRepository( private val stakeKitRepository: StakeKitRepository, private val p2pEthPoolRepository: P2PEthPoolRepository, - private val stakingBalanceStoreV2: YieldsBalancesStore, + private val stakingBalanceStoreV2: StakingBalancesStore, private val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, private val stakingFeatureToggles: StakingFeatureToggles, @@ -106,13 +106,13 @@ internal class DefaultStakingRepository( return withContext(dispatchers.default) { val balances = stakingBalanceStoreV2.getAllSyncOrNull(userWalletId) ?: return@withContext false - val hasDataYieldBalance by lazy { - balances.any { yieldBalance -> - (yieldBalance as? YieldBalance.Data)?.balance?.items?.isNotEmpty() == true + val hasDataStakingBalance by lazy { + balances.any { stakingBalance -> + stakingBalance is StakingBalance.Data } } - balances.isNotEmpty() && hasDataYieldBalance + balances.isNotEmpty() && hasDataStakingBalance } } @@ -135,7 +135,7 @@ internal class DefaultStakingRepository( address = address, ), ) - if (balance != null && balance is YieldBalance.Data && balance.balance.items.isNotEmpty()) { + if ((balance as? StakingBalance.Data.StakeKit)?.balance?.items?.isNotEmpty() == true) { return true } else { stakingFeatureToggles.isCardanoStakingEnabled diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PStakingBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PStakingBalanceConverter.kt new file mode 100644 index 0000000000..83f9d0d2f7 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PStakingBalanceConverter.kt @@ -0,0 +1,60 @@ +package com.tangem.data.staking.converters.ethpool + +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.P2PEthPoolExitRequestDTO +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolStakeDTO +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.staking.* +import com.tangem.domain.staking.model.StakingIntegrationID +import kotlinx.datetime.Instant + +/** Converts P2P ETH Pool API response to [StakingBalance.Data.P2P] */ +internal object P2PStakingBalanceConverter { + + fun convert(response: P2PEthPoolAccountResponse, source: StatusSource): StakingBalance.Data.P2P { + val stakingId = StakingID( + integrationId = StakingIntegrationID.P2P.EthereumPooled.value, + address = response.delegatorAddress, + ) + + val account = P2PStakingAccount( + delegatorAddress = response.delegatorAddress, + vaultAddress = response.vaultAddress, + stake = convertStake(response.stake), + availableToUnstake = response.availableToUnstake, + availableToWithdraw = response.availableToWithdraw, + exitQueue = convertExitQueue(response.exitQueue), + ) + + return StakingBalance.Data.P2P( + stakingId = stakingId, + source = source, + account = account, + ) + } + + private fun convertStake(dto: P2PEthPoolStakeDTO): P2PStake { + return P2PStake( + assets = dto.assets, + totalEarnedAssets = dto.totalEarnedAssets, + ) + } + + private fun convertExitQueue(dto: P2PEthPoolExitQueueDTO): P2PExitQueue { + return P2PExitQueue( + total = dto.total.toBigDecimal(), + requests = dto.requests.map(::convertExitRequest), + ) + } + + private fun convertExitRequest(dto: P2PEthPoolExitRequestDTO): P2PExitRequest { + return P2PExitRequest( + ticket = dto.ticket, + totalAssets = dto.totalAssets.toBigDecimal(), + timestamp = Instant.fromEpochSeconds(dto.timestamp), + withdrawalTimestamp = Instant.fromEpochSeconds(dto.withdrawalTimestamp), + isClaimable = dto.isClaimable, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PYieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PYieldBalanceConverter.kt new file mode 100644 index 0000000000..373a5de22f --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/ethpool/P2PYieldBalanceConverter.kt @@ -0,0 +1,92 @@ +package com.tangem.data.staking.converters.ethpool + +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.staking.* +import com.tangem.domain.staking.model.ethpool.P2PEthPoolAccount +import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault +import java.math.BigDecimal + +/** + * tmp solution before facade implementation + */ +internal object P2PYieldBalanceConverter { + + private const val ETH_DECIMALS = 18 + private const val ETH_SYMBOL = "ETH" + private const val ETH_NAME = "Ethereum" + private const val ETH_COINGECKO_ID = "ethereum" + + fun convert( + account: P2PEthPoolAccount, + vault: P2PEthPoolVault, + address: String, + source: StatusSource, + ): YieldBalance { + val integrationId = "p2p-ethereum-pooled" + val stakingId = StakingID( + integrationId = integrationId, + address = address, + ) + + val balanceItems = buildBalanceItems(account, vault) + + return if (balanceItems.isEmpty()) { + YieldBalance.Empty(stakingId = stakingId, source = source) + } else { + YieldBalance.Data( + stakingId = stakingId, + source = source, + balance = YieldBalanceItem( + items = balanceItems, + integrationId = integrationId, + ), + ) + } + } + + private fun buildBalanceItems(account: P2PEthPoolAccount, vault: P2PEthPoolVault): List = buildList { + if (account.stake.assets > BigDecimal.ZERO) { + add( + createBalanceItem( + groupId = "p2p-staked", + amount = account.stake.assets, + type = BalanceType.STAKED, + validatorAddress = vault.vaultAddress, + ), + ) + } + } + + private fun createBalanceItem( + groupId: String, + amount: BigDecimal, + type: BalanceType, + validatorAddress: String, + ): BalanceItem { + return BalanceItem( + groupId = groupId, + token = createEthToken(), + type = type, + amount = amount, + rawCurrencyId = ETH_COINGECKO_ID, + validatorAddress = validatorAddress, + date = null, + pendingActions = emptyList(), + pendingActionsConstraints = emptyList(), + isPending = false, + ) + } + + private fun createEthToken(): YieldToken { + return YieldToken( + name = ETH_NAME, + network = NetworkType.ETHEREUM, + symbol = ETH_SYMBOL, + decimals = ETH_DECIMALS, + address = null, + coinGeckoId = ETH_COINGECKO_ID, + logoURI = null, + isPoints = false, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceFetcherModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceFetcherModule.kt new file mode 100644 index 0000000000..958f675654 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceFetcherModule.kt @@ -0,0 +1,24 @@ +package com.tangem.data.staking.di + +import com.tangem.data.staking.multi.DefaultMultiStakingBalanceFetcher +import com.tangem.data.staking.single.DefaultSingleStakingBalanceFetcher +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher +import com.tangem.domain.staking.single.SingleStakingBalanceFetcher +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface StakingBalanceFetcherModule { + + @Binds + @Singleton + fun bindSingleStakingBalanceFetcher(impl: DefaultSingleStakingBalanceFetcher): SingleStakingBalanceFetcher + + @Binds + @Singleton + fun bindMultiStakingBalanceFetcher(impl: DefaultMultiStakingBalanceFetcher): MultiStakingBalanceFetcher +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceProducerFactoryModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceProducerFactoryModule.kt new file mode 100644 index 0000000000..48d46bc193 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceProducerFactoryModule.kt @@ -0,0 +1,28 @@ +package com.tangem.data.staking.di + +import com.tangem.data.staking.multi.DefaultMultiStakingBalanceProducer +import com.tangem.data.staking.single.DefaultSingleStakingBalanceProducer +import com.tangem.domain.staking.multi.MultiStakingBalanceProducer +import com.tangem.domain.staking.single.SingleStakingBalanceProducer +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface StakingBalanceProducerFactoryModule { + + @Binds + @Singleton + fun bindSingleStakingBalanceProducerFactory( + impl: DefaultSingleStakingBalanceProducer.Factory, + ): SingleStakingBalanceProducer.Factory + + @Binds + @Singleton + fun bindMultiStakingBalanceProducerFactory( + impl: DefaultMultiStakingBalanceProducer.Factory, + ): MultiStakingBalanceProducer.Factory +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceSupplierModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceSupplierModule.kt new file mode 100644 index 0000000000..26afc00827 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceSupplierModule.kt @@ -0,0 +1,79 @@ +package com.tangem.data.staking.di + +import androidx.datastore.core.DataStore +import com.tangem.data.staking.store.DefaultP2PBalancesStore +import com.tangem.data.staking.store.DefaultStakingBalancesStore +import com.tangem.data.staking.store.P2PBalancesStore +import com.tangem.data.staking.store.StakingBalancesStore +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.staking.multi.MultiStakingBalanceProducer +import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier +import com.tangem.domain.staking.single.SingleStakingBalanceProducer +import com.tangem.domain.staking.single.SingleStakingBalanceSupplier +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object StakingBalanceSupplierModule { + + @Provides + @Singleton + fun provideStakingBalancesStore( + persistenceStore: DataStore>>, + dispatchers: CoroutineDispatcherProvider, + ): StakingBalancesStore { + return DefaultStakingBalancesStore( + runtimeStore = RuntimeSharedStore(), + persistenceStore = persistenceStore, + dispatchers = dispatchers, + ) + } + + @Provides + @Singleton + fun provideP2PBalancesStore( + persistenceStore: DataStore>>, + dispatchers: CoroutineDispatcherProvider, + ): P2PBalancesStore { + return DefaultP2PBalancesStore( + runtimeStore = RuntimeSharedStore(), + persistenceStore = persistenceStore, + dispatchers = dispatchers, + ) + } + + @Provides + @Singleton + fun provideSingleStakingBalanceSupplier( + factory: SingleStakingBalanceProducer.Factory, + ): SingleStakingBalanceSupplier { + return object : SingleStakingBalanceSupplier( + factory = factory, + keyCreator = { params -> + listOf( + "single_staking_balance", + params.userWalletId.stringValue, + params.stakingId.integrationId, + params.stakingId.address, + ) + .joinToString(separator = "_") + }, + ) {} + } + + @Provides + @Singleton + fun provideMultiStakingBalanceSupplier(factory: MultiStakingBalanceProducer.Factory): MultiStakingBalanceSupplier { + return object : MultiStakingBalanceSupplier( + factory = factory, + keyCreator = { "multi_staking_balances_${it.userWalletId.stringValue}" }, + ) {} + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 92c1ecf98d..22164269fd 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -5,7 +5,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.data.staking.* import com.tangem.data.staking.converters.error.StakeKitErrorConverter -import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.store.StakingBalancesStore import com.tangem.data.staking.toggles.DefaultStakingFeatureToggles import com.tangem.data.staking.utils.DefaultStakingCleaner import com.tangem.datasource.api.ethpool.P2PEthPoolApi @@ -55,7 +55,7 @@ internal object StakingDataModule { fun provideStakingRepository( stakeKitRepository: StakeKitRepository, p2pEthPoolRepository: P2PEthPoolRepository, - yieldsBalancesStore: YieldsBalancesStore, + stakingBalancesStore: StakingBalancesStore, dispatchers: CoroutineDispatcherProvider, getUserWalletUseCase: GetUserWalletUseCase, stakingFeatureToggles: StakingFeatureToggles, @@ -64,7 +64,7 @@ internal object StakingDataModule { return DefaultStakingRepository( stakeKitRepository = stakeKitRepository, p2pEthPoolRepository = p2pEthPoolRepository, - stakingBalanceStoreV2 = yieldsBalancesStore, + stakingBalanceStoreV2 = stakingBalancesStore, dispatchers = dispatchers, getUserWalletUseCase = getUserWalletUseCase, walletManagersFacade = walletManagersFacade, @@ -134,11 +134,11 @@ internal object StakingDataModule { @Provides @Singleton fun provideStakingCleaner( - yieldsBalancesStore: YieldsBalancesStore, + stakingBalancesStore: StakingBalancesStore, dispatchers: CoroutineDispatcherProvider, ): StakingCleaner { return DefaultStakingCleaner( - yieldsBalancesStore = yieldsBalancesStore, + stakingBalancesStore = stakingBalancesStore, dispatchers = dispatchers, ) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceFetcherModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceFetcherModule.kt deleted file mode 100644 index 9224d037e3..0000000000 --- a/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceFetcherModule.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.data.staking.di - -import com.tangem.data.staking.multi.DefaultMultiYieldBalanceFetcher -import com.tangem.data.staking.single.DefaultSingleYieldBalanceFetcher -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher -import com.tangem.domain.staking.single.SingleYieldBalanceFetcher -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface YieldBalanceFetcherModule { - - @Binds - @Singleton - fun bindSingleYieldBalanceFetcher(impl: DefaultSingleYieldBalanceFetcher): SingleYieldBalanceFetcher - - @Binds - @Singleton - fun bindMultiYieldBalanceFetcher(impl: DefaultMultiYieldBalanceFetcher): MultiYieldBalanceFetcher -} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceProducerFactoryModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceProducerFactoryModule.kt deleted file mode 100644 index 5f737af9ee..0000000000 --- a/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceProducerFactoryModule.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.data.staking.di - -import com.tangem.data.staking.multi.DefaultMultiYieldBalanceProducer -import com.tangem.data.staking.single.DefaultSingleYieldBalanceProducer -import com.tangem.domain.staking.multi.MultiYieldBalanceProducer -import com.tangem.domain.staking.single.SingleYieldBalanceProducer -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface YieldBalanceProducerFactoryModule { - - @Binds - @Singleton - fun bindSingleYieldBalanceProducerFactory( - impl: DefaultSingleYieldBalanceProducer.Factory, - ): SingleYieldBalanceProducer.Factory - - @Binds - @Singleton - fun bindMultiYieldBalanceProducerFactory( - impl: DefaultMultiYieldBalanceProducer.Factory, - ): MultiYieldBalanceProducer.Factory -} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceSupplierModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceSupplierModule.kt deleted file mode 100644 index 3ef90f5b74..0000000000 --- a/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceSupplierModule.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.tangem.data.staking.di - -import androidx.datastore.core.DataStore -import com.tangem.data.staking.store.DefaultYieldsBalancesStore -import com.tangem.data.staking.store.YieldsBalancesStore -import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO -import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.domain.staking.multi.MultiYieldBalanceProducer -import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier -import com.tangem.domain.staking.single.SingleYieldBalanceProducer -import com.tangem.domain.staking.single.SingleYieldBalanceSupplier -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object YieldBalanceSupplierModule { - - @Provides - @Singleton - fun provideYieldsBalancesStore( - persistenceStore: DataStore>>, - dispatchers: CoroutineDispatcherProvider, - ): YieldsBalancesStore { - return DefaultYieldsBalancesStore( - runtimeStore = RuntimeSharedStore(), - persistenceStore = persistenceStore, - dispatchers = dispatchers, - ) - } - - @Provides - @Singleton - fun provideSingleYieldBalanceSupplier(factory: SingleYieldBalanceProducer.Factory): SingleYieldBalanceSupplier { - return object : SingleYieldBalanceSupplier( - factory = factory, - keyCreator = { params -> - listOf( - "single_yield_balance", - params.userWalletId.stringValue, - params.stakingId.integrationId, - params.stakingId.address, - ) - .joinToString(separator = "_") - }, - ) {} - } - - @Provides - @Singleton - fun provideMultiYieldBalanceSupplier(factory: MultiYieldBalanceProducer.Factory): MultiYieldBalanceSupplier { - return object : MultiYieldBalanceSupplier( - factory = factory, - keyCreator = { "multi_yields_balances_${it.userWalletId.stringValue}" }, - ) {} - } -} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt new file mode 100644 index 0000000000..8a2411fc11 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt @@ -0,0 +1,344 @@ +package com.tangem.data.staking.multi + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import arrow.core.toOption +import com.tangem.data.common.api.safeApiCall +import com.tangem.data.staking.store.P2PBalancesStore +import com.tangem.data.staking.store.StakingBalancesStore +import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.ethpool.P2PEthPoolApi +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse +import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO +import com.tangem.datasource.local.token.P2PEthPoolVaultsStore +import com.tangem.datasource.local.token.StakingYieldsStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.core.utils.catchOn +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.staking.model.StakingIntegrationID +import com.tangem.domain.staking.model.ethpool.P2PStakingConfig +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +/** + * Default implementation of [MultiStakingBalanceFetcher] + * + * Supports both StakeKit and P2P staking providers. + * + * @property userWalletsStore user wallets store + * @property stakingYieldsStore staking yields store + * @property stakingBalancesStore staking balances store (StakeKit) + * @property p2pBalancesStore P2P balances store + * @property stakeKitApi stake kit API + * @property p2pApi P2P ETH Pool API + * @property p2pVaultsStore P2P vaults store + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +@Suppress("LongParameterList") +internal class DefaultMultiStakingBalanceFetcher @Inject constructor( + private val userWalletsStore: UserWalletsStore, + private val stakingYieldsStore: StakingYieldsStore, + private val stakingBalancesStore: StakingBalancesStore, + private val p2pBalancesStore: P2PBalancesStore, + private val stakeKitApi: StakeKitApi, + private val p2pApi: P2PEthPoolApi, + private val p2pVaultsStore: P2PEthPoolVaultsStore, + private val dispatchers: CoroutineDispatcherProvider, +) : MultiStakingBalanceFetcher { + + override suspend fun invoke(params: MultiStakingBalanceFetcher.Params): Either { + Timber.i("Start fetching staking balances for params:\n$params") + + val stakingIds = params.stakingIds.ifEmpty { + Timber.i("Nothing to fetch, empty stakingIds for ${params.userWalletId}") + return Unit.right() + } + + checkIsSupportedByWalletOrElse(userWalletId = params.userWalletId) { + return it.left() + } + + val (stakeKitIds, p2pIds) = stakingIds.partition { stakingId -> + val stakingIntegrationID = StakingIntegrationID.entries.find { + it.value == stakingId.integrationId + } + stakingIntegrationID is StakingIntegrationID.StakeKit + } + + Timber.i( + """ + Staking IDs to fetch: + - StakeKit: ${stakeKitIds.joinToString()} + - P2P: ${p2pIds.joinToString()} + """.trimIndent(), + ) + + return Either.catchOn(dispatchers.default) { + coroutineScope { + if (stakeKitIds.isNotEmpty()) { + launch { fetchStakeKitBalances(params.userWalletId, stakeKitIds.toSet()) } + } + + if (p2pIds.isNotEmpty()) { + launch { fetchP2PBalances(params.userWalletId, p2pIds.toSet()) } + } + } + } + .onLeft { throwable -> + Timber.e(throwable, "Unable to fetch staking balances $params") + + if (stakeKitIds.isNotEmpty()) { + stakingBalancesStore.storeError( + userWalletId = params.userWalletId, + stakingIds = stakeKitIds.toSet(), + ) + } + if (p2pIds.isNotEmpty()) { + p2pBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = p2pIds.toSet()) + } + } + } + + private suspend fun fetchStakeKitBalances(userWalletId: UserWalletId, stakingIds: Set) { + stakingBalancesStore.refresh(userWalletId = userWalletId, stakingIds = stakingIds) + + val availableStakingIds = getAvailableStakingIds( + userWalletId = userWalletId, + stakingIds = stakingIds, + ) + + fetchFromStakeKit(userWalletId = userWalletId, stakingIds = availableStakingIds) + } + + private suspend fun fetchP2PBalances(userWalletId: UserWalletId, stakingIds: Set) { + p2pBalancesStore.refresh(userWalletId = userWalletId, stakingIds = stakingIds) + + val vaults = runSuspendCatching { p2pVaultsStore.getSync() }.getOrNull().orEmpty() + if (vaults.isEmpty()) { + Timber.w("No P2P vaults available for $userWalletId") + p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds) + return + } + + fetchFromP2P(userWalletId = userWalletId, stakingIds = stakingIds, vaults = vaults) + } + + private suspend fun fetchFromP2P( + userWalletId: UserWalletId, + stakingIds: Set, + vaults: List, + ) { + safeApiCall( + call = { + val addresses = stakingIds.map { it.address }.toSet() + + val responses = mutableSetOf() + + for (vault in vaults) { + for (address in addresses) { + runSuspendCatching { + val response = p2pApi.getAccountInfo( + network = P2PStakingConfig.activeNetwork.value, + delegatorAddress = address, + vaultAddress = vault.vaultAddress, + ) + + when (response) { + is ApiResponse.Success -> { + val data = response.data + if (data.error != null) { + Timber.w( + "P2P API returned error for vault ${vault.vaultAddress}, " + + "address $address: ${data.error ?: "error"}", + ) + } else { + val result = requireNotNull(data.result) { + "Result is null in successful response" + } + responses.add(result) + } + } + is ApiResponse.Error -> { + Timber.w( + response.cause, + "Failed to fetch P2P balance for vault ${vault.vaultAddress}, " + + "address $address", + ) + } + } + }.onFailure { error -> + Timber.w( + error, + "Failed to fetch P2P balance for vault ${vault.vaultAddress}, address $address", + ) + } + } + } + + Timber.i("Successfully fetched ${responses.size} P2P balances for $userWalletId") + + if (responses.isNotEmpty()) { + p2pBalancesStore.storeActual(userWalletId = userWalletId, values = responses) + + val missingStakingIds = stakingIds.filter { stakingId -> + responses.none { response -> + response.delegatorAddress.equals(stakingId.address, ignoreCase = true) + } + } + + if (missingStakingIds.isNotEmpty()) { + Timber.i("Missing responses for ${missingStakingIds.size} staking IDs: $missingStakingIds") + p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = missingStakingIds.toSet()) + } + } else { + Timber.i("No P2P responses received for $userWalletId") + p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds) + } + }, + onError = { throwable -> + Timber.e(throwable, "Unable to fetch P2P balances $userWalletId") + + p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds) + + throw throwable + }, + ) + } + + private inline fun checkIsSupportedByWalletOrElse(userWalletId: UserWalletId, ifNotSupported: (Throwable) -> Unit) { + val maybeUserWallet = userWalletsStore.getSyncOrNull(key = userWalletId).toOption() + + val isSupportedByWallet = maybeUserWallet.isSome(UserWallet::isMultiCurrency) + + if (!isSupportedByWallet) { + val exception = IllegalStateException("Wallet $userWalletId is not supported: $maybeUserWallet") + Timber.e(exception) + + ifNotSupported(exception) + } + } + + private suspend fun getAvailableStakingIds(userWalletId: UserWalletId, stakingIds: Set): Set { + val yieldIds = getYieldsIds(userWalletId = userWalletId) + + // [true] -> available + // [false] -> unavailable + val groupedStakingIds = stakingIds.groupBy { stakingId -> + yieldIds.any { it == stakingId.integrationId } + } + + val availableStakingIds = groupedStakingIds[true].orEmpty() + val unavailableStakingIds = groupedStakingIds[false].orEmpty() + + Timber.i( + """ + Available staking IDs: ${availableStakingIds.joinToString()} + Unavailable staking IDs: ${unavailableStakingIds.joinToString()} + """.trimIndent(), + ) + + if (unavailableStakingIds.isNotEmpty()) { + stakingBalancesStore.storeError(userWalletId = userWalletId, stakingIds = unavailableStakingIds.toSet()) + } + + return availableStakingIds.toSet().ifEmpty { + val exception = IllegalStateException( + """ + No available yields to fetch yield balances: + – userWalletId: $userWalletId + – stakingIds: ${stakingIds.joinToString()} + """.trimIndent(), + ) + Timber.i(exception) + throw exception + } + } + + private suspend fun getYieldsIds(userWalletId: UserWalletId): Set { + val yieldsIds = stakingYieldsStore.getSyncWithTimeout().orEmpty() + .mapNotNullTo(destination = hashSetOf(), transform = YieldDTO::id) + + if (yieldsIds.isEmpty()) { + val exception = IllegalStateException("No enabled yields for $userWalletId") + Timber.e(exception) + + throw exception + } + + return yieldsIds + } + + private suspend fun fetchFromStakeKit(userWalletId: UserWalletId, stakingIds: Set) { + safeApiCall( + call = { + val requests = stakingIds.map(YieldBalanceRequestBodyFactory::create) + + val yieldBalances = coroutineScope { + requests + // TODO: in the future, consider optimizing this part + .chunked(size = 15) // StakeKitApi limitation: no more than 15 requests at the same time + .map { + async(dispatchers.io) { + stakeKitApi.getMultipleYieldBalances(it).bind() + } + } + .awaitAll() + .flatten() + .toSet() + } + + Timber.i( + "Successfully fetched staking balances for $userWalletId:\n${yieldBalances.joinToString("\n")}", + ) + stakingBalancesStore.storeActual(userWalletId = userWalletId, values = yieldBalances) + + if (!allResponsesReceived(requests, yieldBalances)) { + val values = stakingIds.filter { stakingId -> + yieldBalances.none { balanceWrapper -> + stakingId.integrationId == balanceWrapper.integrationId && + stakingId.address == balanceWrapper.addresses.address + } + } + + stakingBalancesStore.storeError(userWalletId = userWalletId, stakingIds = values.toSet()) + } + }, + onError = { throwable -> + Timber.e(throwable, "Unable to fetch staking balances $userWalletId") + + stakingBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds) + + throw throwable + }, + ) + } + + private fun allResponsesReceived( + requests: List, + yieldBalances: Set, + ): Boolean { + return requests.all { request -> + yieldBalances.any { balance -> + request.integrationId == balance.integrationId && + request.addresses.address == balance.addresses.address + } + } + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceProducer.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceProducer.kt new file mode 100644 index 0000000000..2ab3c5ad8c --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceProducer.kt @@ -0,0 +1,56 @@ +package com.tangem.data.staking.multi + +import arrow.core.Option +import arrow.core.some +import com.tangem.data.staking.store.P2PBalancesStore +import com.tangem.data.staking.store.StakingBalancesStore +import com.tangem.domain.models.staking.StakingBalance +import com.tangem.domain.staking.multi.MultiStakingBalanceProducer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEmpty + +/** + * Default implementation of [MultiStakingBalanceProducer] + * + * Combines staking balances from both StakeKit and P2P providers. + * + * @property params params + * @property stakingBalancesStore StakeKit staking balances store + * @property p2pBalancesStore P2P balances store + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +internal class DefaultMultiStakingBalanceProducer @AssistedInject constructor( + @Assisted val params: MultiStakingBalanceProducer.Params, + private val stakingBalancesStore: StakingBalancesStore, + private val p2pBalancesStore: P2PBalancesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : MultiStakingBalanceProducer { + + override val fallback: Option> = emptySet().some() + + override fun produce(): Flow> { + val stakeKitFlow = stakingBalancesStore.get(userWalletId = params.userWalletId) + val p2pFlow = p2pBalancesStore.get(userWalletId = params.userWalletId) + + return combine(stakeKitFlow, p2pFlow) { stakeKitBalances, p2pBalances -> + stakeKitBalances + p2pBalances + } + .distinctUntilChanged() + .onEmpty { emit(value = hashSetOf()) } + .flowOn(dispatchers.default) + } + + @AssistedFactory + interface Factory : MultiStakingBalanceProducer.Factory { + override fun create(params: MultiStakingBalanceProducer.Params): DefaultMultiStakingBalanceProducer + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt deleted file mode 100644 index eca2f69f33..0000000000 --- a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt +++ /dev/null @@ -1,196 +0,0 @@ -package com.tangem.data.staking.multi - -import arrow.core.Either -import arrow.core.left -import arrow.core.right -import arrow.core.toOption -import com.tangem.data.common.api.safeApiCall -import com.tangem.data.staking.store.YieldsBalancesStore -import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory -import com.tangem.datasource.api.stakekit.StakeKitApi -import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody -import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO -import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO -import com.tangem.datasource.local.token.StakingYieldsStore -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.core.utils.catchOn -import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import timber.log.Timber -import javax.inject.Inject - -/** - * Default implementation of [MultiYieldBalanceFetcher] - * - * @property userWalletsStore user wallets store - * @property stakingYieldsStore staking yields store - * @property yieldsBalancesStore yields balances store - * @property stakeKitApi stake kit API - * @property dispatchers dispatchers - * -[REDACTED_AUTHOR] - */ -internal class DefaultMultiYieldBalanceFetcher @Inject constructor( - private val userWalletsStore: UserWalletsStore, - private val stakingYieldsStore: StakingYieldsStore, - private val yieldsBalancesStore: YieldsBalancesStore, - private val stakeKitApi: StakeKitApi, - private val dispatchers: CoroutineDispatcherProvider, -) : MultiYieldBalanceFetcher { - - override suspend fun invoke(params: MultiYieldBalanceFetcher.Params): Either { - Timber.i("Start fetching yield balances for params:\n$params") - - val stakingIds = params.stakingIds.ifEmpty { - Timber.i("Nothing to fetch, empty stakingIds for ${params.userWalletId}") - return Unit.right() - } - - checkIsSupportedByWalletOrElse(userWalletId = params.userWalletId) { - return it.left() - } - - Timber.i("Staking IDs to fetch:\n${stakingIds.joinToString("\n")}") - - return Either.catchOn(dispatchers.default) { - yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = stakingIds) - - val availableStakingIds = getAvailableStakingIds( - userWalletId = params.userWalletId, - stakingIds = stakingIds, - ) - - fetch(userWalletId = params.userWalletId, stakingIds = availableStakingIds) - } - .onLeft { throwable -> - Timber.e(throwable, "Unable to fetch yield balances $params") - - yieldsBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = stakingIds) - } - } - - private inline fun checkIsSupportedByWalletOrElse(userWalletId: UserWalletId, ifNotSupported: (Throwable) -> Unit) { - val maybeUserWallet = userWalletsStore.getSyncOrNull(key = userWalletId).toOption() - - val isSupportedByWallet = maybeUserWallet.isSome(UserWallet::isMultiCurrency) - - if (!isSupportedByWallet) { - val exception = IllegalStateException("Wallet $userWalletId is not supported: $maybeUserWallet") - Timber.e(exception) - - ifNotSupported(exception) - } - } - - private suspend fun getAvailableStakingIds(userWalletId: UserWalletId, stakingIds: Set): Set { - val yieldIds = getYieldsIds(userWalletId = userWalletId) - - // [true] -> available - // [false] -> unavailable - val groupedStakingIds = stakingIds.groupBy { stakingId -> - yieldIds.any { it == stakingId.integrationId } - } - - val availableStakingIds = groupedStakingIds[true].orEmpty() - val unavailableStakingIds = groupedStakingIds[false].orEmpty() - - Timber.i( - """ - Available staking IDs: ${availableStakingIds.joinToString()} - Unavailable staking IDs: ${unavailableStakingIds.joinToString()} - """.trimIndent(), - ) - - if (unavailableStakingIds.isNotEmpty()) { - yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = unavailableStakingIds.toSet()) - } - - return availableStakingIds.toSet().ifEmpty { - val exception = IllegalStateException( - """ - No available yields to fetch yield balances: - – userWalletId: $userWalletId - – stakingIds: ${stakingIds.joinToString()} - """.trimIndent(), - ) - Timber.i(exception) - throw exception - } - } - - private suspend fun getYieldsIds(userWalletId: UserWalletId): Set { - val yieldsIds = stakingYieldsStore.getSyncWithTimeout().orEmpty() - .mapNotNullTo(destination = hashSetOf(), transform = YieldDTO::id) - - if (yieldsIds.isEmpty()) { - val exception = IllegalStateException("No enabled yields for $userWalletId") - Timber.e(exception) - - throw exception - } - - return yieldsIds - } - - private suspend fun fetch(userWalletId: UserWalletId, stakingIds: Set) { - safeApiCall( - call = { - val requests = stakingIds.map(YieldBalanceRequestBodyFactory::create) - - val yieldBalances = coroutineScope { - requests - // TODO: in the future, consider optimizing this part - .chunked(size = 15) // StakeKitApi limitation: no more than 15 requests at the same time - .map { - async(dispatchers.io) { - stakeKitApi.getMultipleYieldBalances(it).bind() - } - } - .awaitAll() - .flatten() - .toSet() - } - - Timber.i("Successfully fetched yield balances for $userWalletId:\n${yieldBalances.joinToString("\n")}") - yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = yieldBalances) - - if (!allResponsesReceived(requests, yieldBalances)) { - val values = stakingIds.filter { stakingId -> - yieldBalances.none { balanceWrapper -> - stakingId.integrationId == balanceWrapper.integrationId && - stakingId.address == balanceWrapper.addresses.address - } - } - - yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = values.toSet()) - } - }, - onError = { throwable -> - Timber.e(throwable, "Unable to fetch yield balances $userWalletId") - - yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds) - - throw throwable - }, - ) - } - - private fun allResponsesReceived( - requests: List, - yieldBalances: Set, - ): Boolean { - return requests.all { request -> - yieldBalances.any { balance -> - request.integrationId == balance.integrationId && - request.addresses.address == balance.addresses.address - } - } - } -} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt deleted file mode 100644 index f66ec71196..0000000000 --- a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.data.staking.multi - -import arrow.core.Option -import arrow.core.some -import com.tangem.data.staking.store.YieldsBalancesStore -import com.tangem.domain.models.staking.YieldBalance -import com.tangem.domain.staking.multi.MultiYieldBalanceProducer -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.onEmpty - -/** - * Default implementation of [MultiYieldBalanceProducer] - * - * @property params params - * @property yieldsBalancesStore yields balances store - * @property dispatchers dispatchers - * -[REDACTED_AUTHOR] - */ -internal class DefaultMultiYieldBalanceProducer @AssistedInject constructor( - @Assisted val params: MultiYieldBalanceProducer.Params, - private val yieldsBalancesStore: YieldsBalancesStore, - private val dispatchers: CoroutineDispatcherProvider, -) : MultiYieldBalanceProducer { - - override val fallback: Option> = emptySet().some() - - override fun produce(): Flow> { - return yieldsBalancesStore.get(userWalletId = params.userWalletId) - .distinctUntilChanged() - .onEmpty { emit(value = hashSetOf()) } - .flowOn(dispatchers.default) - } - - @AssistedFactory - interface Factory : MultiYieldBalanceProducer.Factory { - override fun create(params: MultiYieldBalanceProducer.Params): DefaultMultiYieldBalanceProducer - } -} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceFetcher.kt new file mode 100644 index 0000000000..52742732b9 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceFetcher.kt @@ -0,0 +1,27 @@ +package com.tangem.data.staking.single + +import arrow.core.Either +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher +import com.tangem.domain.staking.single.SingleStakingBalanceFetcher +import javax.inject.Inject + +/** + * Default implementation of [SingleStakingBalanceFetcher] + * + * @property multiStakingBalanceFetcher multi staking balance fetcher + * +[REDACTED_AUTHOR] + */ +internal class DefaultSingleStakingBalanceFetcher @Inject constructor( + private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, +) : SingleStakingBalanceFetcher { + + override suspend fun invoke(params: SingleStakingBalanceFetcher.Params): Either { + return multiStakingBalanceFetcher( + params = MultiStakingBalanceFetcher.Params( + userWalletId = params.userWalletId, + stakingIds = setOf(params.stakingId), + ), + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducer.kt similarity index 61% rename from data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt rename to data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducer.kt index 3dccb55dfc..2bde73daf2 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducer.kt @@ -4,10 +4,10 @@ import arrow.core.Option import arrow.core.some import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.ExceptionAnalyticsEvent -import com.tangem.domain.models.staking.YieldBalance -import com.tangem.domain.staking.multi.MultiYieldBalanceProducer -import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier -import com.tangem.domain.staking.single.SingleYieldBalanceProducer +import com.tangem.domain.models.staking.StakingBalance +import com.tangem.domain.staking.multi.MultiStakingBalanceProducer +import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier +import com.tangem.domain.staking.single.SingleStakingBalanceProducer import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.indexOfFirstOrNull import dagger.assisted.Assisted @@ -20,29 +20,29 @@ import kotlinx.coroutines.flow.mapNotNull import timber.log.Timber /** - * Default implementation of [SingleYieldBalanceProducer] + * Default implementation of [SingleStakingBalanceProducer] * - * @property params params - * @property multiYieldBalanceSupplier multi yield balance supplier - * @property analyticsExceptionHandler analytics exception handler - * @property dispatchers dispatchers + * @property params params + * @property multiStakingBalanceSupplier multi staking balance supplier + * @property analyticsExceptionHandler analytics exception handler + * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ -internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( - @Assisted private val params: SingleYieldBalanceProducer.Params, - private val multiYieldBalanceSupplier: MultiYieldBalanceSupplier, +internal class DefaultSingleStakingBalanceProducer @AssistedInject constructor( + @Assisted private val params: SingleStakingBalanceProducer.Params, + private val multiStakingBalanceSupplier: MultiStakingBalanceSupplier, private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val dispatchers: CoroutineDispatcherProvider, -) : SingleYieldBalanceProducer { +) : SingleStakingBalanceProducer { - override val fallback: Option = YieldBalance.Error(stakingId = params.stakingId).some() + override val fallback: Option = StakingBalance.Error(stakingId = params.stakingId).some() - override fun produce(): Flow { - Timber.i("Producing yield balance for params:\n$params") + override fun produce(): Flow { + Timber.i("Producing staking balance for params:\n$params") - return multiYieldBalanceSupplier( - params = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId), + return multiStakingBalanceSupplier( + params = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId), ) .mapNotNull { balances -> val currentStakingId = params.stakingId @@ -65,7 +65,7 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( currentBalances.joinToString("\n"), ) - val dataIndex = currentBalances.indexOfFirstOrNull { it is YieldBalance.Data } + val dataIndex = currentBalances.indexOfFirstOrNull { it is StakingBalance.Data } if (dataIndex != null) { currentBalances[dataIndex] @@ -75,7 +75,7 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( } else { val balance = currentBalances.firstOrNull() ?: return@mapNotNull null - Timber.i("Yield balance found for $currentStakingId:\n$balance") + Timber.i("Staking balance found for $currentStakingId:\n$balance") balance } } @@ -84,7 +84,7 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( } @AssistedFactory - interface Factory : SingleYieldBalanceProducer.Factory { - override fun create(params: SingleYieldBalanceProducer.Params): DefaultSingleYieldBalanceProducer + interface Factory : SingleStakingBalanceProducer.Factory { + override fun create(params: SingleStakingBalanceProducer.Params): DefaultSingleStakingBalanceProducer } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt deleted file mode 100644 index 1666b20e35..0000000000 --- a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.data.staking.single - -import arrow.core.Either -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher -import com.tangem.domain.staking.single.SingleYieldBalanceFetcher -import javax.inject.Inject - -/** - * Default implementation of [MultiYieldBalanceFetcher] - * - * @property multiYieldBalanceFetcher multi yield balance fetcher - * -[REDACTED_AUTHOR] - */ -internal class DefaultSingleYieldBalanceFetcher @Inject constructor( - private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, -) : SingleYieldBalanceFetcher { - - override suspend fun invoke(params: SingleYieldBalanceFetcher.Params): Either { - return multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = params.userWalletId, - stakingIds = setOf(params.stakingId), - ), - ) - } -} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/DefaultP2PBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/DefaultP2PBalancesStore.kt new file mode 100644 index 0000000000..d95a730a9f --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/store/DefaultP2PBalancesStore.kt @@ -0,0 +1,191 @@ +package com.tangem.data.staking.store + +import androidx.datastore.core.DataStore +import com.tangem.data.staking.converters.ethpool.P2PStakingBalanceConverter +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.staking.StakingBalance +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.staking.model.StakingIntegrationID +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +internal typealias WalletIdWithP2PStakingBalances = Map> +internal typealias WalletIdWithP2PResponses = Map> + +/** + * Default implementation of [P2PBalancesStore] + * + * Stores P2P ETH Pool staking balances. + * + * @property runtimeStore runtime store + * @property persistenceStore persistence store + * @param dispatchers coroutine dispatchers + */ +internal class DefaultP2PBalancesStore( + private val runtimeStore: RuntimeSharedStore, + private val persistenceStore: DataStore, + dispatchers: CoroutineDispatcherProvider, +) : P2PBalancesStore { + + private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io) + + init { + scope.launch { + val cachedData = persistenceStore.data.firstOrNull() ?: return@launch + + runtimeStore.store( + value = cachedData.map { (stringWalletId, responses) -> + val key = UserWalletId(stringWalletId) + val value = responses.map { response -> + P2PStakingBalanceConverter.convert( + response = response, + source = StatusSource.CACHE, + ) + }.toSet() + + key to value + }.toMap(), + ) + } + } + + override fun get(userWalletId: UserWalletId): Flow> { + return runtimeStore.get().map { it[userWalletId].orEmpty() } + } + + override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): StakingBalance? { + return runtimeStore.getSyncOrNull() + ?.get(userWalletId) + ?.firstOrNull { it.stakingId == stakingId } + } + + override suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? { + return runtimeStore.getSyncOrNull()?.get(userWalletId) + } + + override suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID) { + refresh(userWalletId = userWalletId, stakingIds = setOf(stakingId)) + } + + override suspend fun refresh(userWalletId: UserWalletId, stakingIds: Set) { + updateInRuntime(userWalletId = userWalletId, stakingIds = stakingIds) { + it.copySealed(source = StatusSource.CACHE) + } + } + + override suspend fun storeActual(userWalletId: UserWalletId, values: Set) { + coroutineScope { + launch { storeInRuntime(userWalletId = userWalletId, values = values) } + launch { storeInPersistence(userWalletId = userWalletId, values = values) } + } + } + + override suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set) { + updateInRuntime( + userWalletId = userWalletId, + stakingIds = stakingIds, + ifNotFound = ::createErrorStakingBalance, + update = { it.copySealed(source = StatusSource.ONLY_CACHE) }, + ) + } + + override suspend fun clear(userWalletId: UserWalletId, stakingIds: Set) { + coroutineScope { + launch { clearInRuntime(userWalletId = userWalletId, stakingIds = stakingIds) } + launch { clearInPersistence(userWalletId = userWalletId, stakingIds = stakingIds) } + } + } + + private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set) { + val newBalances = values.map { response -> + P2PStakingBalanceConverter.convert( + response = response, + source = StatusSource.ACTUAL, + ) + }.toSet() + + runtimeStore.update(default = emptyMap()) { saved -> + saved.toMutableMap().apply { + this[userWalletId] = saved[userWalletId] + ?.addOrReplace(newBalances) { old, new -> old.stakingId == new.stakingId } + ?: newBalances + } + } + } + + private suspend fun storeInPersistence(userWalletId: UserWalletId, values: Set) { + persistenceStore.updateData { current -> + current.toMutableMap().apply { + this[userWalletId.stringValue] = this[userWalletId.stringValue] + ?.addOrReplace(values) { old, new -> + old.delegatorAddress == new.delegatorAddress && old.vaultAddress == new.vaultAddress + } + ?: values + } + } + } + + private suspend fun clearInRuntime(userWalletId: UserWalletId, stakingIds: Set) { + runtimeStore.update(default = emptyMap()) { stored -> + stored.toMutableMap().apply { + this[userWalletId] = this[userWalletId].orEmpty() + .filterNot { it.stakingId in stakingIds } + .toSet() + } + } + } + + private suspend fun clearInPersistence(userWalletId: UserWalletId, stakingIds: Set) { + val integrationIds = stakingIds.map { it.integrationId }.toSet() + + persistenceStore.updateData { current -> + current.toMutableMap().apply { + this[userWalletId.stringValue] = this[userWalletId.stringValue].orEmpty() + .filterNot { response -> + StakingIntegrationID.P2P.EthereumPooled.value in integrationIds + } + .toSet() + } + } + } + + private suspend fun updateInRuntime( + userWalletId: UserWalletId, + stakingIds: Set, + ifNotFound: (StakingID) -> StakingBalance? = { null }, + update: (StakingBalance) -> StakingBalance, + ) { + runtimeStore.update(default = emptyMap()) { stored -> + stored.toMutableMap().apply { + val portfolioBalances = stored[userWalletId].orEmpty() + + val balances = stakingIds.mapNotNullTo(hashSetOf()) { stakingId -> + val balance = portfolioBalances + .firstOrNull { it.stakingId == stakingId } + ?: ifNotFound(stakingId) + ?: return@mapNotNullTo null + + update(balance) + } + + val updatedBalances = portfolioBalances.addOrReplace(items = balances) { old, new -> + old.stakingId == new.stakingId + } + + put(key = userWalletId, value = updatedBalances) + } + } + } + + private fun createErrorStakingBalance(id: StakingID): StakingBalance = StakingBalance.Error(stakingId = id) +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/DefaultStakingBalancesStore.kt similarity index 84% rename from data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt rename to data/staking/src/main/java/com/tangem/data/staking/store/DefaultStakingBalancesStore.kt index fa753503be..b2b0ba10a9 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/store/DefaultStakingBalancesStore.kt @@ -3,10 +3,10 @@ package com.tangem.data.staking.store import androidx.datastore.core.DataStore import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.token.converter.YieldBalanceConverter +import com.tangem.datasource.local.token.converter.StakingBalanceConverter import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace @@ -19,10 +19,10 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch internal typealias WalletIdWithWrappers = Map> -internal typealias WalletIdWithBalances = Map> +internal typealias WalletIdWithStakingBalances = Map> /** - * Default implementation of [YieldsBalancesStore] + * Default implementation of [StakingBalancesStore] * * @property runtimeStore runtime store * @property persistenceStore persistence store @@ -30,11 +30,11 @@ internal typealias WalletIdWithBalances = Map> * [REDACTED_AUTHOR] */ -internal class DefaultYieldsBalancesStore( - private val runtimeStore: RuntimeSharedStore, +internal class DefaultStakingBalancesStore( + private val runtimeStore: RuntimeSharedStore, private val persistenceStore: DataStore, dispatchers: CoroutineDispatcherProvider, -) : YieldsBalancesStore { +) : StakingBalancesStore { private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io) @@ -45,7 +45,7 @@ internal class DefaultYieldsBalancesStore( runtimeStore.store( value = cachedStatuses.map { (stringWalletId, wrappers) -> val key = UserWalletId(stringWalletId) - val value = YieldBalanceConverter(isCached = true).convertSet(input = wrappers) + val value = StakingBalanceConverter(isCached = true).convertSet(input = wrappers) .filterNotNull() .toSet() @@ -56,17 +56,17 @@ internal class DefaultYieldsBalancesStore( } } - override fun get(userWalletId: UserWalletId): Flow> { + override fun get(userWalletId: UserWalletId): Flow> { return runtimeStore.get().map { it[userWalletId].orEmpty() } } - override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): YieldBalance? { + override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): StakingBalance? { return runtimeStore.getSyncOrNull() ?.get(userWalletId) ?.firstOrNull { it.stakingId == stakingId } } - override suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? { + override suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? { return runtimeStore.getSyncOrNull()?.get(userWalletId) } @@ -91,7 +91,7 @@ internal class DefaultYieldsBalancesStore( updateInRuntime( userWalletId = userWalletId, stakingIds = stakingIds, - ifNotFound = ::createErrorYieldBalance, + ifNotFound = ::createErrorStakingBalance, update = { it.copySealed(source = StatusSource.ONLY_CACHE) }, ) } @@ -107,7 +107,7 @@ internal class DefaultYieldsBalancesStore( } private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set) { - val newBalances = YieldBalanceConverter(isCached = false).convertSet(input = values) + val newBalances = StakingBalanceConverter(isCached = false).convertSet(input = values) .filterNotNull() .toSet() @@ -140,8 +140,8 @@ internal class DefaultYieldsBalancesStore( private suspend fun updateInRuntime( userWalletId: UserWalletId, stakingIds: Set, - ifNotFound: (StakingID) -> YieldBalance? = { null }, - update: (YieldBalance) -> YieldBalance, + ifNotFound: (StakingID) -> StakingBalance? = { null }, + update: (StakingBalance) -> StakingBalance, ) { runtimeStore.update(default = emptyMap()) { stored -> stored.toMutableMap().apply { @@ -165,7 +165,7 @@ internal class DefaultYieldsBalancesStore( } } - private fun createErrorYieldBalance(id: StakingID): YieldBalance = YieldBalance.Error(stakingId = id) + private fun createErrorStakingBalance(id: StakingID): StakingBalance = StakingBalance.Error(stakingId = id) private fun YieldBalanceWrapperDTO.getStakingId(): StakingID? { val integrationId = integrationId diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/P2PBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/P2PBalancesStore.kt new file mode 100644 index 0000000000..02479075c3 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/store/P2PBalancesStore.kt @@ -0,0 +1,29 @@ +package com.tangem.data.staking.store + +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse +import com.tangem.domain.models.staking.StakingBalance +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow + +/** + * Store for P2P ETH Pool staking balances + */ +interface P2PBalancesStore { + + fun get(userWalletId: UserWalletId): Flow> + + suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): StakingBalance? + + suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? + + suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID) + + suspend fun refresh(userWalletId: UserWalletId, stakingIds: Set) + + suspend fun storeActual(userWalletId: UserWalletId, values: Set) + + suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set) + + suspend fun clear(userWalletId: UserWalletId, stakingIds: Set) +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/YieldsBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/StakingBalancesStore.kt similarity index 51% rename from data/staking/src/main/java/com/tangem/data/staking/store/YieldsBalancesStore.kt rename to data/staking/src/main/java/com/tangem/data/staking/store/StakingBalancesStore.kt index 10bfa99242..5d730dd5b7 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/store/YieldsBalancesStore.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/store/StakingBalancesStore.kt @@ -1,39 +1,27 @@ package com.tangem.data.staking.store import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow -/** - * Store of [YieldBalance]'s set - * -[REDACTED_AUTHOR] - */ -interface YieldsBalancesStore { +/** Store of StakeKit [StakingBalance] */ +interface StakingBalancesStore { - /** Get flow of [YieldBalance]'s set by [userWalletId] */ - fun get(userWalletId: UserWalletId): Flow> + fun get(userWalletId: UserWalletId): Flow> - /** Get [YieldBalance] by [userWalletId] and [stakingId] synchronously or null */ - suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): YieldBalance? + suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): StakingBalance? - /** Get all [YieldBalance] by [userWalletId] synchronously or null */ - suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? + suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? - /** Refresh balance of [stakingId] by [userWalletId] */ suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID) - /** Refresh balances of [stakingIds] by [userWalletId] */ suspend fun refresh(userWalletId: UserWalletId, stakingIds: Set) - /** Store actual [values] by [userWalletId] */ suspend fun storeActual(userWalletId: UserWalletId, values: Set) - /** Store error by [userWalletId] and [stakingIds] */ suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set) - /** Clear balances of [stakingIds] by [userWalletId] */ suspend fun clear(userWalletId: UserWalletId, stakingIds: Set) } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt b/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt index 1aa6753150..c55bf2154e 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt @@ -1,6 +1,6 @@ package com.tangem.data.staking.utils -import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.store.StakingBalancesStore import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.utils.StakingCleaner @@ -9,13 +9,13 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider /** * Default implementation of [StakingCleaner]. * - * @property yieldsBalancesStore Store to manage yields balances. + * @property stakingBalancesStore Store to manage staking balances. * @property dispatchers Coroutine dispatchers provider. * [REDACTED_AUTHOR] */ internal class DefaultStakingCleaner( - private val yieldsBalancesStore: YieldsBalancesStore, + private val stakingBalancesStore: StakingBalancesStore, private val dispatchers: CoroutineDispatcherProvider, ) : StakingCleaner { @@ -23,7 +23,7 @@ internal class DefaultStakingCleaner( if (stakingIds.isEmpty()) return with(dispatchers.default) { - yieldsBalancesStore.clear(userWalletId, stakingIds) + stakingBalancesStore.clear(userWalletId, stakingIds) } } } \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/StakingBalanceExt.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/StakingBalanceExt.kt new file mode 100644 index 0000000000..a0bacfe34f --- /dev/null +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/StakingBalanceExt.kt @@ -0,0 +1,19 @@ +package com.tangem.data.staking + +import com.tangem.data.staking.converters.ethpool.P2PStakingBalanceConverter +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.local.token.converter.StakingBalanceConverter +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.staking.StakingBalance + +internal fun YieldBalanceWrapperDTO.toDomain(source: StatusSource = StatusSource.CACHE): StakingBalance { + return StakingBalanceConverter(isCached = source == StatusSource.CACHE).convert(this)!! +} + +internal fun P2PEthPoolAccountResponse.toDomain(source: StatusSource = StatusSource.CACHE): StakingBalance.Data.P2P { + return P2PStakingBalanceConverter.convert( + response = this, + source = source, + ) +} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt deleted file mode 100644 index 09b56e3b9c..0000000000 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.data.staking - -import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO -import com.tangem.datasource.local.token.converter.YieldBalanceConverter -import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.staking.YieldBalance - -internal fun YieldBalanceWrapperDTO.toDomain(source: StatusSource = StatusSource.CACHE): YieldBalance { - return YieldBalanceConverter(source = source).convert(this)!! -} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt similarity index 64% rename from data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt rename to data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt index f7c56f3dc3..e380157061 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt @@ -4,17 +4,20 @@ import arrow.core.toOption import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.common.test.data.staking.MockYieldDTOFactory import com.tangem.common.test.domain.wallet.MockUserWalletFactory -import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.store.P2PBalancesStore +import com.tangem.data.staking.store.StakingBalancesStore import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.ethpool.P2PEthPoolApi import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.local.token.P2PEthPoolVaultsStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.test.core.assertEitherLeft import com.tangem.test.core.assertEitherRight import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider @@ -28,30 +31,36 @@ import org.junit.jupiter.api.TestInstance [REDACTED_AUTHOR] */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class DefaultMultiYieldBalanceFetcherTest { +internal class DefaultMultiStakingBalanceFetcherTest { private val userWalletsStore: UserWalletsStore = mockk() private val stakingYieldsStore: StakingYieldsStore = mockk() - private val yieldsBalancesStore: YieldsBalancesStore = mockk(relaxUnitFun = true) + private val stakingBalancesStore: StakingBalancesStore = mockk(relaxUnitFun = true) + private val p2pBalancesStore: P2PBalancesStore = mockk(relaxUnitFun = true) private val stakeKitApi: StakeKitApi = mockk() + private val p2pApi: P2PEthPoolApi = mockk() + private val p2pVaultsStore: P2PEthPoolVaultsStore = mockk() - private val fetcher = DefaultMultiYieldBalanceFetcher( + private val fetcher = DefaultMultiStakingBalanceFetcher( userWalletsStore = userWalletsStore, stakingYieldsStore = stakingYieldsStore, - yieldsBalancesStore = yieldsBalancesStore, + stakingBalancesStore = stakingBalancesStore, + p2pBalancesStore = p2pBalancesStore, stakeKitApi = stakeKitApi, + p2pApi = p2pApi, + p2pVaultsStore = p2pVaultsStore, dispatchers = TestingCoroutineDispatcherProvider(), ) @BeforeEach fun resetMocks() { - clearMocks(userWalletsStore, stakingYieldsStore, yieldsBalancesStore, stakeKitApi) + clearMocks(userWalletsStore, stakingYieldsStore, stakingBalancesStore, stakeKitApi) } @Test - fun `fetch yields balances successfully`() = runTest { + fun `fetch staking balances successfully`() = runTest { // Arrange - val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet @@ -72,21 +81,21 @@ internal class DefaultMultiYieldBalanceFetcherTest { // Assert coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) + stakingBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getMultipleYieldBalances(requests) - yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result) + stakingBalancesStore.storeActual(userWalletId = userWalletId, values = result) } - coVerify(inverse = true) { yieldsBalancesStore.storeError(any(), any()) } + coVerify(inverse = true) { stakingBalancesStore.storeError(any(), any()) } assertEitherRight(actual) } @Test - fun `fetch yields balances successfully if one of stakingIds is unavailable`() = runTest { + fun `fetch staking balances successfully if one of stakingIds is unavailable`() = runTest { // Arrange - val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet @@ -104,20 +113,20 @@ internal class DefaultMultiYieldBalanceFetcherTest { // Assert coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) + stakingBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() - yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(solanaId)) + stakingBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(solanaId)) stakeKitApi.getMultipleYieldBalances(requests) - yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result) + stakingBalancesStore.storeActual(userWalletId = userWalletId, values = result) } assertEitherRight(actual) } @Test - fun `fetch yields balances failure if user wallet is not supported`() = runTest { + fun `fetch staking balances failure if user wallet is not supported`() = runTest { // Arrange - val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet @@ -129,11 +138,11 @@ internal class DefaultMultiYieldBalanceFetcherTest { coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) } coVerify(inverse = true) { - yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any()) + stakingBalancesStore.refresh(userWalletId = any(), stakingIds = any()) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) - yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) - yieldsBalancesStore.storeError(userWalletId = any(), stakingIds = any()) + stakingBalancesStore.storeActual(userWalletId = any(), values = any()) + stakingBalancesStore.storeError(userWalletId = any(), stakingIds = any()) } val expected = IllegalStateException("Wallet ${params.userWalletId} is not supported: ${userWallet.toOption()}") @@ -142,9 +151,9 @@ internal class DefaultMultiYieldBalanceFetcherTest { } @Test - fun `fetch yields balances failure if userWalletsStore returns null`() = runTest { + fun `fetch staking balances failure if userWalletsStore returns null`() = runTest { // Arrange - val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns null @@ -155,11 +164,11 @@ internal class DefaultMultiYieldBalanceFetcherTest { coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) } coVerify(inverse = true) { - yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any()) + stakingBalancesStore.refresh(userWalletId = any(), stakingIds = any()) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) - yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) - yieldsBalancesStore.storeError(userWalletId = any(), stakingIds = any()) + stakingBalancesStore.storeActual(userWalletId = any(), values = any()) + stakingBalancesStore.storeError(userWalletId = any(), stakingIds = any()) } val expected = IllegalStateException("Wallet ${params.userWalletId} is not supported: ${null.toOption()}") @@ -168,9 +177,9 @@ internal class DefaultMultiYieldBalanceFetcherTest { } @Test - fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns null`() = runTest { + fun `fetch staking balances failure if stakingYieldsStore getSyncWithTimeout returns null`() = runTest { // Arrange - val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingYieldsStore.getSyncWithTimeout() } returns null @@ -181,14 +190,14 @@ internal class DefaultMultiYieldBalanceFetcherTest { // Assert coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) + stakingBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() - yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) + stakingBalancesStore.storeError(userWalletId, tonAndSolanaIds) } coVerify(inverse = true) { stakeKitApi.getMultipleYieldBalances(any()) - yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + stakingBalancesStore.storeActual(userWalletId = any(), values = any()) } val expected = IllegalStateException("No enabled yields for ${params.userWalletId}") @@ -197,9 +206,9 @@ internal class DefaultMultiYieldBalanceFetcherTest { } @Test - fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns empty list`() = runTest { + fun `fetch staking balances failure if stakingYieldsStore getSyncWithTimeout returns empty list`() = runTest { // Arrange - val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingYieldsStore.getSyncWithTimeout() } returns emptyList() @@ -210,14 +219,14 @@ internal class DefaultMultiYieldBalanceFetcherTest { // Assert coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) + stakingBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() - yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) + stakingBalancesStore.storeError(userWalletId, tonAndSolanaIds) } coVerify(inverse = true) { stakeKitApi.getMultipleYieldBalances(any()) - yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + stakingBalancesStore.storeActual(userWalletId = any(), values = any()) } val expected = IllegalStateException("No enabled yields for ${params.userWalletId}") @@ -226,9 +235,9 @@ internal class DefaultMultiYieldBalanceFetcherTest { } @Test - fun `fetch yields balances failure if yields converting is failed`() = runTest { + fun `fetch staking balances failure if yields converting is failed`() = runTest { // Arrange - val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet @@ -244,14 +253,14 @@ internal class DefaultMultiYieldBalanceFetcherTest { // Assert coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) + stakingBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() - yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) + stakingBalancesStore.storeError(userWalletId, tonAndSolanaIds) } coVerify(inverse = true) { stakeKitApi.getMultipleYieldBalances(any()) - yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + stakingBalancesStore.storeActual(userWalletId = any(), values = any()) } val expected = IllegalStateException("No enabled yields for ${params.userWalletId}") @@ -260,9 +269,9 @@ internal class DefaultMultiYieldBalanceFetcherTest { } @Test - fun `fetch yields balances failure if available yields does not contain ids from params`() = runTest { + fun `fetch staking balances failure if available yields does not contain ids from params`() = runTest { // Arrange - val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet @@ -275,14 +284,14 @@ internal class DefaultMultiYieldBalanceFetcherTest { // Assert coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) + stakingBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() - yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) + stakingBalancesStore.storeError(userWalletId, tonAndSolanaIds) } coVerify(inverse = true) { stakeKitApi.getMultipleYieldBalances(any()) - yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + stakingBalancesStore.storeActual(userWalletId = any(), values = any()) } val expected = IllegalStateException( @@ -297,9 +306,9 @@ internal class DefaultMultiYieldBalanceFetcherTest { } @Test - fun `fetch yields balances failure if stakeKitApi getMultipleYieldBalances is failed`() = runTest { + fun `fetch staking balances failure if stakeKitApi getMultipleYieldBalances is failed`() = runTest { // Arrange - val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet @@ -320,13 +329,13 @@ internal class DefaultMultiYieldBalanceFetcherTest { // Assert coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) + stakingBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getMultipleYieldBalances(requests) - yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + stakingBalancesStore.storeError(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) } - coVerify(inverse = true) { yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) } + coVerify(inverse = true) { stakingBalancesStore.storeActual(userWalletId = any(), values = any()) } val expected = ApiResponseError.NetworkException() diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceProducerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceProducerTest.kt new file mode 100644 index 0000000000..faf28b5731 --- /dev/null +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceProducerTest.kt @@ -0,0 +1,283 @@ +package com.tangem.data.staking.multi + +import com.google.common.truth.Truth +import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory +import com.tangem.common.test.data.staking.MockP2PEthPoolAccountResponseFactory +import com.tangem.data.staking.store.P2PBalancesStore +import com.tangem.data.staking.store.StakingBalancesStore +import com.tangem.data.staking.toDomain +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.staking.* +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.staking.model.StakingIntegrationID +import com.tangem.domain.staking.multi.MultiStakingBalanceProducer +import com.tangem.test.core.getEmittedValues +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultMultiStakingBalanceProducerTest { + + private val params = MultiStakingBalanceProducer.Params(userWalletId = UserWalletId("011")) + + private val stakingBalancesStore = mockk() + private val p2pBalancesStore = mockk() + private val dispatchers = TestingCoroutineDispatcherProvider() + + private val producer = DefaultMultiStakingBalanceProducer( + params = params, + stakingBalancesStore = stakingBalancesStore, + p2pBalancesStore = p2pBalancesStore, + dispatchers = dispatchers, + ) + + @Test + fun `test that flow is mapped for user wallet id from params`() = runTest { + val balances = setOf( + MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(), + MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(), + ) + + val networksStatusesFlow = flowOf(balances) + + every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet()) + + val actual = producer.produce() + + // check after producer.produce() + verify { stakingBalancesStore.get(params.userWalletId) } + verify { p2pBalancesStore.get(params.userWalletId) } + + val values = getEmittedValues(flow = actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values.first()).isEqualTo(balances) + } + + @Test + fun `test that flow is updated if balances are updated`() = runTest { + val networksStatusesFlow = MutableSharedFlow>(replay = 2) + + every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet()) + + val actual = producer.produce() + + // check after producer.produce() + verify { stakingBalancesStore.get(params.userWalletId) } + verify { p2pBalancesStore.get(params.userWalletId) } + + // first emit + val balances = setOf( + MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(tonId).toDomain(), + MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(solanaId).toDomain(), + ) + + networksStatusesFlow.emit(balances) + + val values1 = getEmittedValues(flow = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1.first()).isEqualTo(balances) + + // second emit + val updatedWrappers = setOf( + MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(), + MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(), + ) + + networksStatusesFlow.emit(updatedWrappers) + + val values2 = getEmittedValues(flow = actual) + + val expected = listOf(balances, updatedWrappers) + Truth.assertThat(values2.size).isEqualTo(2) + Truth.assertThat(values2).isEqualTo(expected) + } + + @Test + fun `test that flow is filtered the same balance`() = runTest { + val networksStatusesFlow = MutableSharedFlow>(replay = 2) + + every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet()) + + val actual = producer.produce() + + // check after producer.produce() + verify { stakingBalancesStore.get(params.userWalletId) } + verify { p2pBalancesStore.get(params.userWalletId) } + + // first emit + val wrappers = setOf( + MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(tonId).toDomain(), + MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(solanaId).toDomain(), + ) + + networksStatusesFlow.emit(wrappers) + + val values1 = getEmittedValues(flow = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1.first()).isEqualTo(wrappers) + + // second emit + networksStatusesFlow.emit(wrappers) + + val values2 = getEmittedValues(flow = actual) + + Truth.assertThat(values2.size).isEqualTo(1) + Truth.assertThat(values2.first()).isEqualTo(wrappers) + } + + @Test + fun `test if flow throws exception`() = runTest { + val exception = IllegalStateException() + val balances = setOf( + MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(), + MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(), + ) + + val innerFlow = MutableStateFlow(value = false) + val networksStatusesFlow = flow { + if (innerFlow.value) { + emit(balances) + } else { + throw exception + } + } + .buffer(capacity = 5) + + every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow + every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet()) + + val actual = producer.produceWithFallback() + + // check after producer.produce() + verify { stakingBalancesStore.get(params.userWalletId) } + verify { p2pBalancesStore.get(params.userWalletId) } + + val values1 = getEmittedValues(flow = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1).isEqualTo(listOf(emptySet())) + + innerFlow.emit(value = true) + + val values2 = getEmittedValues(flow = actual) + + Truth.assertThat(values2.size).isEqualTo(1) + Truth.assertThat(values2).isEqualTo(listOf(balances)) + } + + @Test + fun `test that flow is empty`() = runTest { + every { stakingBalancesStore.get(params.userWalletId) } returns emptyFlow() + every { p2pBalancesStore.get(params.userWalletId) } returns emptyFlow() + + val actual = producer.produce() + + // check after producer.produce() + verify { stakingBalancesStore.get(params.userWalletId) } + verify { p2pBalancesStore.get(params.userWalletId) } + + val values = getEmittedValues(flow = actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values).isEqualTo(listOf(emptySet())) + } + + @Test + fun `test that StakeKit and P2P balances are combined`() = runTest { + val stakeKitBalances = createStakeKitBalances() + val p2pBalances = createP2PBalances() + + every { stakingBalancesStore.get(params.userWalletId) } returns flowOf(stakeKitBalances) + every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(p2pBalances) + + val actual = producer.produce() + + // check after producer.produce() + verify { stakingBalancesStore.get(params.userWalletId) } + verify { p2pBalancesStore.get(params.userWalletId) } + + val values = getEmittedValues(flow = actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values.first()).isEqualTo(stakeKitBalances + p2pBalances) + } + + @Test + fun `test that P2P balances are updated independently from StakeKit`() = runTest { + val stakeKitBalances = createStakeKitBalancesWithTonOnly() + val p2pFlow = MutableSharedFlow>(replay = 2) + + every { stakingBalancesStore.get(params.userWalletId) } returns flowOf(stakeKitBalances) + every { p2pBalancesStore.get(params.userWalletId) } returns p2pFlow + + val actual = producer.produce() + + // check after producer.produce() + verify { stakingBalancesStore.get(params.userWalletId) } + verify { p2pBalancesStore.get(params.userWalletId) } + + // first emit - empty P2P + p2pFlow.emit(emptySet()) + + val values1 = getEmittedValues(flow = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1.first()).isEqualTo(stakeKitBalances) + + // second emit - with P2P balance + val p2pBalances = createP2PBalances() + p2pFlow.emit(p2pBalances) + + val values2 = getEmittedValues(flow = actual) + + Truth.assertThat(values2.size).isEqualTo(2) + Truth.assertThat(values2.last()).isEqualTo(stakeKitBalances + p2pBalances) + } + + private companion object { + + val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId + val solanaId = StakingID( + integrationId = "solana-sol-native-multivalidator-staking", + address = "0x1", + ) + val p2pEthereumId = StakingID( + integrationId = StakingIntegrationID.P2P.EthereumPooled.value, + address = "0x5aa711F440Eb6d4361148bBD89d03464628ace84", + ) + + fun createStakeKitBalances(): Set { + return setOf( + MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(), + MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(), + ) + } + + fun createStakeKitBalancesWithTonOnly(): Set { + return setOf( + MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(), + ) + } + + fun createP2PBalances(): Set { + return setOf( + MockP2PEthPoolAccountResponseFactory.createWithBalance(stakingId = p2pEthereumId).toDomain( + source = StatusSource.ACTUAL, + ), + ) + } + } +} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt deleted file mode 100644 index 08fada9524..0000000000 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt +++ /dev/null @@ -1,191 +0,0 @@ -package com.tangem.data.staking.multi - -import com.google.common.truth.Truth -import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory -import com.tangem.data.staking.store.YieldsBalancesStore -import com.tangem.data.staking.toDomain -import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.multi.MultiYieldBalanceProducer -import com.tangem.test.core.getEmittedValues -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.test.runTest -import org.junit.Test - -/** -[REDACTED_AUTHOR] - */ -internal class DefaultMultiYieldBalanceProducerTest { - - private val params = MultiYieldBalanceProducer.Params(userWalletId = UserWalletId("011")) - - private val yieldsBalancesStore = mockk() - private val dispatchers = TestingCoroutineDispatcherProvider() - - private val producer = DefaultMultiYieldBalanceProducer( - params = params, - yieldsBalancesStore = yieldsBalancesStore, - dispatchers = dispatchers, - ) - - @Test - fun `test that flow is mapped for user wallet id from params`() = runTest { - val balances = setOf( - MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(), - MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(), - ) - - val networksStatusesFlow = flowOf(balances) - - every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow - - val actual = producer.produce() - - // check after producer.produce() - verify { yieldsBalancesStore.get(params.userWalletId) } - - val values = getEmittedValues(flow = actual) - - Truth.assertThat(values.size).isEqualTo(1) - Truth.assertThat(values.first()).isEqualTo(balances) - } - - @Test - fun `test that flow is updated if balances are updated`() = runTest { - val networksStatusesFlow = MutableSharedFlow>(replay = 2) - - every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow - - val actual = producer.produce() - - // check after producer.produce() - verify { yieldsBalancesStore.get(params.userWalletId) } - - // first emit - val balances = setOf( - MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(tonId).toDomain(), - MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(solanaId).toDomain(), - ) - - networksStatusesFlow.emit(balances) - - val values1 = getEmittedValues(flow = actual) - - Truth.assertThat(values1.size).isEqualTo(1) - Truth.assertThat(values1.first()).isEqualTo(balances) - - // second emit - val updatedWrappers = setOf( - MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(), - MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(), - ) - - networksStatusesFlow.emit(updatedWrappers) - - val values2 = getEmittedValues(flow = actual) - - val expected = listOf(balances, updatedWrappers) - Truth.assertThat(values2.size).isEqualTo(2) - Truth.assertThat(values2).isEqualTo(expected) - } - - @Test - fun `test that flow is filtered the same balance`() = runTest { - val networksStatusesFlow = MutableSharedFlow>(replay = 2) - - every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow - - val actual = producer.produce() - - // check after producer.produce() - verify { yieldsBalancesStore.get(params.userWalletId) } - - // first emit - val wrappers = setOf( - MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(tonId).toDomain(), - MockYieldBalanceWrapperDTOFactory.createWithEmptyBalance(solanaId).toDomain(), - ) - - networksStatusesFlow.emit(wrappers) - - val values1 = getEmittedValues(flow = actual) - - Truth.assertThat(values1.size).isEqualTo(1) - Truth.assertThat(values1.first()).isEqualTo(wrappers) - - // second emit - networksStatusesFlow.emit(wrappers) - - val values2 = getEmittedValues(flow = actual) - - Truth.assertThat(values2.size).isEqualTo(1) - Truth.assertThat(values2.first()).isEqualTo(wrappers) - } - - @Test - fun `test if flow throws exception`() = runTest { - val exception = IllegalStateException() - val balances = setOf( - MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain(), - MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(), - ) - - val innerFlow = MutableStateFlow(value = false) - val networksStatusesFlow = flow { - if (innerFlow.value) { - emit(balances) - } else { - throw exception - } - } - .buffer(capacity = 5) - - every { yieldsBalancesStore.get(params.userWalletId) } returns networksStatusesFlow - - val actual = producer.produceWithFallback() - - // check after producer.produce() - verify { yieldsBalancesStore.get(params.userWalletId) } - - val values1 = getEmittedValues(flow = actual) - - Truth.assertThat(values1.size).isEqualTo(1) - Truth.assertThat(values1).isEqualTo(listOf(emptySet())) - - innerFlow.emit(value = true) - - val values2 = getEmittedValues(flow = actual) - - Truth.assertThat(values2.size).isEqualTo(1) - Truth.assertThat(values2).isEqualTo(listOf(balances)) - } - - @Test - fun `test that flow is empty`() = runTest { - every { yieldsBalancesStore.get(params.userWalletId) } returns emptyFlow() - - val actual = producer.produce() - - // check after producer.produce() - verify { yieldsBalancesStore.get(params.userWalletId) } - - val values = getEmittedValues(flow = actual) - - Truth.assertThat(values.size).isEqualTo(1) - Truth.assertThat(values).isEqualTo(listOf(emptySet())) - } - - private companion object { - - val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId - val solanaId = StakingID( - integrationId = "solana-sol-native-multivalidator-staking", - address = "0x1", - ) - } -} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleStakingBalanceFetcherTest.kt similarity index 51% rename from data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt rename to data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleStakingBalanceFetcherTest.kt index 6722fc8d5f..9638e95898 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleStakingBalanceFetcherTest.kt @@ -5,8 +5,8 @@ import arrow.core.right import com.google.common.truth.Truth import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher -import com.tangem.domain.staking.single.SingleYieldBalanceFetcher +import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher +import com.tangem.domain.staking.single.SingleStakingBalanceFetcher import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.coVerify @@ -20,32 +20,32 @@ import org.junit.jupiter.api.TestInstance [REDACTED_AUTHOR] */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class DefaultSingleYieldBalanceFetcherTest { +internal class DefaultSingleStakingBalanceFetcherTest { - private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher = mockk() + private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher = mockk() - private val fetcher = DefaultSingleYieldBalanceFetcher( - multiYieldBalanceFetcher = multiYieldBalanceFetcher, + private val fetcher = DefaultSingleStakingBalanceFetcher( + multiStakingBalanceFetcher = multiStakingBalanceFetcher, ) @BeforeEach fun resetMocks() { - clearMocks(multiYieldBalanceFetcher) + clearMocks(multiStakingBalanceFetcher) } @Test - fun `fetch yield balance successfully`() = runTest { + fun `fetch staking balance successfully`() = runTest { // Arrange - val params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = tonId) + val params = SingleStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingId = tonId) - val multiParams = MultiYieldBalanceFetcher.Params( + val multiParams = MultiStakingBalanceFetcher.Params( userWalletId = userWalletId, stakingIds = setOf(tonId), ) val multiResult = Unit.right() - coEvery { multiYieldBalanceFetcher(params = multiParams) } returns multiResult + coEvery { multiStakingBalanceFetcher(params = multiParams) } returns multiResult // Act val actual = fetcher.invoke(params).isRight() @@ -53,26 +53,26 @@ internal class DefaultSingleYieldBalanceFetcherTest { // Assert Truth.assertThat(actual).isTrue() - coVerify { multiYieldBalanceFetcher(params = multiParams) } + coVerify { multiStakingBalanceFetcher(params = multiParams) } } @Test - fun `fetch yield balance failure`() = runTest { + fun `fetch staking balance failure`() = runTest { // Arrange - val params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = tonId) + val params = SingleStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingId = tonId) - val multiParams = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = setOf(tonId)) + val multiParams = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = setOf(tonId)) val multiResult = IllegalStateException().left() - coEvery { multiYieldBalanceFetcher(params = multiParams) } returns multiResult + coEvery { multiStakingBalanceFetcher(params = multiParams) } returns multiResult // Act val actual = fetcher.invoke(params) // Assert Truth.assertThat(actual).isEqualTo(multiResult) - coVerify { multiYieldBalanceFetcher(params = multiParams) } + coVerify { multiStakingBalanceFetcher(params = multiParams) } } private companion object { diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducerTest.kt similarity index 79% rename from data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt rename to data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducerTest.kt index aa9e56d44a..9c42102d3e 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducerTest.kt @@ -4,12 +4,12 @@ import com.google.common.truth.Truth import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.data.staking.toDomain +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.multi.MultiYieldBalanceProducer -import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier -import com.tangem.domain.staking.single.SingleYieldBalanceProducer +import com.tangem.domain.staking.multi.MultiStakingBalanceProducer +import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier +import com.tangem.domain.staking.single.SingleStakingBalanceProducer import com.tangem.test.core.getEmittedValues import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.clearMocks @@ -26,20 +26,20 @@ import org.junit.jupiter.api.TestInstance [REDACTED_AUTHOR] */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class DefaultSingleYieldBalanceProducerTest { +internal class DefaultSingleStakingBalanceProducerTest { - private val params = SingleYieldBalanceProducer.Params( + private val params = SingleStakingBalanceProducer.Params( userWalletId = UserWalletId(stringValue = "011"), stakingId = tonId, ) - private val multiNetworkStatusSupplier = mockk() + private val multiNetworkStatusSupplier = mockk() private val analyticsExceptionHandler = mockk(relaxUnitFun = true) private val dispatchers = TestingCoroutineDispatcherProvider() - private val producer = DefaultSingleYieldBalanceProducer( + private val producer = DefaultSingleStakingBalanceProducer( params = params, - multiYieldBalanceSupplier = multiNetworkStatusSupplier, + multiStakingBalanceSupplier = multiNetworkStatusSupplier, analyticsExceptionHandler = analyticsExceptionHandler, dispatchers = dispatchers, ) @@ -61,7 +61,7 @@ internal class DefaultSingleYieldBalanceProducerTest { ), ) - val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) + val multiParams = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId) every { multiNetworkStatusSupplier(multiParams) } returns multiFlow // Act @@ -74,17 +74,17 @@ internal class DefaultSingleYieldBalanceProducerTest { } @Test - fun `flow is updated if yield balance is updated`() = runTest { + fun `flow is updated if staking balance is updated`() = runTest { // Arrange - val multiFlow = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + val multiFlow = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) - val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) + val multiParams = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId) every { multiNetworkStatusSupplier(multiParams) } returns multiFlow val producerFlow = producer.produceWithFallback() val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() - val updatedBalance = YieldBalance.Error(stakingId = tonId) + val updatedBalance = StakingBalance.Error(stakingId = tonId) // Act (first emit) multiFlow.emit(value = setOf(balance)) @@ -108,9 +108,9 @@ internal class DefaultSingleYieldBalanceProducerTest { @Test fun `flow is filtered the same status`() = runTest { // Arrange - val multiFlow = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + val multiFlow = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) - val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) + val multiParams = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId) every { multiNetworkStatusSupplier(multiParams) } returns multiFlow val producerFlow = producer.produceWithFallback() @@ -153,7 +153,7 @@ internal class DefaultSingleYieldBalanceProducerTest { } .buffer(capacity = 5) - val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) + val multiParams = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId) every { multiNetworkStatusSupplier(multiParams) } returns multiFlow val producerFlow = producer.produceWithFallback() @@ -162,7 +162,7 @@ internal class DefaultSingleYieldBalanceProducerTest { val actual1 = getEmittedValues(flow = producerFlow) // Assert (first emit) - val fallbackStatus = YieldBalance.Error(stakingId = tonId.copy(address = "0x1")) + val fallbackStatus = StakingBalance.Error(stakingId = tonId.copy(address = "0x1")) Truth.assertThat(actual1).hasSize(1) Truth.assertThat(actual1).containsExactly(fallbackStatus) @@ -184,7 +184,7 @@ internal class DefaultSingleYieldBalanceProducerTest { val multiFlow = flowOf(setOf(balance)) - val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) + val multiParams = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId) every { multiNetworkStatusSupplier(multiParams) } returns multiFlow val producerFlow = producer.produce() diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreGetMethodTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/store/StakingBalancesStoreGetMethodTest.kt similarity index 84% rename from data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreGetMethodTest.kt rename to data/staking/src/test/kotlin/com/tangem/data/staking/store/StakingBalancesStoreGetMethodTest.kt index 09366ecbcf..8ca3111517 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreGetMethodTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/store/StakingBalancesStoreGetMethodTest.kt @@ -5,7 +5,7 @@ import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.common.test.datastore.MockStateDataStore import com.tangem.data.staking.toDomain import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.test.core.getEmittedValues import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider @@ -15,12 +15,12 @@ import org.junit.Test /** [REDACTED_AUTHOR] */ -internal class YieldsBalancesStoreGetMethodTest { +internal class StakingBalancesStoreGetMethodTest { - private val runtimeStore = RuntimeSharedStore() + private val runtimeStore = RuntimeSharedStore() private val persistenceStore = MockStateDataStore(default = emptyMap()) - private val store = DefaultYieldsBalancesStore( + private val store = DefaultStakingBalancesStore( runtimeStore = runtimeStore, persistenceStore = persistenceStore, dispatchers = TestingCoroutineDispatcherProvider(), @@ -32,7 +32,7 @@ internal class YieldsBalancesStoreGetMethodTest { val values = getEmittedValues(flow = actual) - val expected = listOf(emptySet()) + val expected = listOf(emptySet()) Truth.assertThat(values).isEqualTo(expected) } @@ -44,7 +44,7 @@ internal class YieldsBalancesStoreGetMethodTest { val values = getEmittedValues(flow = actual) - val expected = listOf(emptySet()) + val expected = listOf(emptySet()) Truth.assertThat(values).isEqualTo(expected) } @@ -59,7 +59,7 @@ internal class YieldsBalancesStoreGetMethodTest { val values = getEmittedValues(flow = actual) Truth.assertThat(values.size).isEqualTo(1) - Truth.assertThat(values).isEqualTo(listOf(emptySet())) + Truth.assertThat(values).isEqualTo(listOf(emptySet())) } @Test diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreInitializationTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/store/StakingBalancesStoreInitializationTest.kt similarity index 82% rename from data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreInitializationTest.kt rename to data/staking/src/test/kotlin/com/tangem/data/staking/store/StakingBalancesStoreInitializationTest.kt index 8885ab72bc..cf08a1f8e3 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreInitializationTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/store/StakingBalancesStoreInitializationTest.kt @@ -6,7 +6,7 @@ import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.common.test.datastore.MockStateDataStore import com.tangem.data.staking.toDomain import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.every @@ -18,16 +18,16 @@ import org.junit.Test /** [REDACTED_AUTHOR] */ -internal class YieldsBalancesStoreInitializationTest { +internal class StakingBalancesStoreInitializationTest { @Test fun `test initialization if cache store is empty`() = runTest { - val runtimeStore = RuntimeSharedStore() + val runtimeStore = RuntimeSharedStore() val persistenceStore: DataStore = mockk() every { persistenceStore.data } returns emptyFlow() - DefaultYieldsBalancesStore( + DefaultStakingBalancesStore( runtimeStore = runtimeStore, persistenceStore = persistenceStore, dispatchers = TestingCoroutineDispatcherProvider(), @@ -38,21 +38,21 @@ internal class YieldsBalancesStoreInitializationTest { @Test fun `test initialization if cache store contains empty map`() = runTest { - val runtimeStore = RuntimeSharedStore() + val runtimeStore = RuntimeSharedStore() val persistenceStore = MockStateDataStore(default = emptyMap()) - DefaultYieldsBalancesStore( + DefaultStakingBalancesStore( runtimeStore = runtimeStore, persistenceStore = persistenceStore, dispatchers = TestingCoroutineDispatcherProvider(), ) - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(emptyMap>()) + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(emptyMap>()) } @Test fun `test initialization if cache store is not empty`() = runTest { - val runtimeStore = RuntimeSharedStore() + val runtimeStore = RuntimeSharedStore() val persistenceStore = MockStateDataStore(default = emptyMap()) val wrapper = MockYieldBalanceWrapperDTOFactory.createWithBalance() @@ -63,7 +63,7 @@ internal class YieldsBalancesStoreInitializationTest { } } - DefaultYieldsBalancesStore( + DefaultStakingBalancesStore( runtimeStore = runtimeStore, persistenceStore = persistenceStore, dispatchers = TestingCoroutineDispatcherProvider(), diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreUpdateMethodsTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/store/StakingBalancesStoreUpdateMethodsTest.kt similarity index 92% rename from data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreUpdateMethodsTest.kt rename to data/staking/src/test/kotlin/com/tangem/data/staking/store/StakingBalancesStoreUpdateMethodsTest.kt index febababb02..18d1b25415 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreUpdateMethodsTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/store/StakingBalancesStoreUpdateMethodsTest.kt @@ -7,8 +7,8 @@ import com.tangem.data.staking.toDomain import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import kotlinx.coroutines.flow.firstOrNull @@ -18,12 +18,12 @@ import org.junit.Test /** [REDACTED_AUTHOR] */ -internal class YieldsBalancesStoreUpdateMethodsTest { +internal class StakingBalancesStoreUpdateMethodsTest { - private val runtimeStore = RuntimeSharedStore() + private val runtimeStore = RuntimeSharedStore() private val persistenceStore = MockStateDataStore(default = emptyMap()) - private val store = DefaultYieldsBalancesStore( + private val store = DefaultStakingBalancesStore( runtimeStore = runtimeStore, persistenceStore = persistenceStore, dispatchers = TestingCoroutineDispatcherProvider(), @@ -33,7 +33,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest { fun `refresh the single id if runtime store is empty`() = runTest { store.refresh(userWalletId = userWalletId, stakingId = stakingId) - val runtimeExpected = mapOf(userWalletId to emptySet()) + val runtimeExpected = mapOf(userWalletId to emptySet()) Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) @@ -63,7 +63,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest { fun `refresh the multi ids if runtime store is empty`() = runTest { store.refresh(userWalletId = userWalletId, stakingIds = stakingIds) - val runtimeExpected = mapOf(userWalletId to emptySet()) + val runtimeExpected = mapOf(userWalletId to emptySet()) Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) @@ -129,7 +129,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest { store.storeError(userWalletId = userWalletId, stakingIds = setOf(stakingId)) val runtimeExpected = mapOf( - userWalletId to setOf(YieldBalance.Error(stakingId)), + userWalletId to setOf(StakingBalance.Error(stakingId)), ) Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/utils/DefaultStakingCleanerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/utils/DefaultStakingCleanerTest.kt index 32751b6fc6..630865f7ba 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/utils/DefaultStakingCleanerTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/utils/DefaultStakingCleanerTest.kt @@ -1,6 +1,6 @@ package com.tangem.data.staking.utils -import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.store.StakingBalancesStore import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingIntegrationID @@ -16,9 +16,9 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DefaultStakingCleanerTest { - private val yieldsBalancesStore = mockk(relaxed = true) + private val stakingBalancesStore = mockk(relaxed = true) private val cleaner = DefaultStakingCleaner( - yieldsBalancesStore = yieldsBalancesStore, + stakingBalancesStore = stakingBalancesStore, dispatchers = TestingCoroutineDispatcherProvider(), ) private val userWalletId = UserWalletId("011") @@ -28,7 +28,7 @@ class DefaultStakingCleanerTest { @BeforeEach fun setUp() { - clearMocks(yieldsBalancesStore) + clearMocks(stakingBalancesStore) } @Test @@ -38,7 +38,7 @@ class DefaultStakingCleanerTest { // Assert coVerifyOrder { - yieldsBalancesStore.clear(userWalletId = userWalletId, stakingIds = stakingIds) + stakingBalancesStore.clear(userWalletId = userWalletId, stakingIds = stakingIds) } } @@ -49,7 +49,7 @@ class DefaultStakingCleanerTest { // Assert coVerifyOrder(inverse = true) { - yieldsBalancesStore.clear(userWalletId = any(), stakingIds = any()) + stakingBalancesStore.clear(userWalletId = any(), stakingIds = any()) } } } \ No newline at end of file diff --git a/data/swap/detekt-baseline-debug.xml b/data/swap/detekt-baseline-debug.xml deleted file mode 100644 index b047ac0eff..0000000000 --- a/data/swap/detekt-baseline-debug.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - MaxChainedCallsOnSameLine:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2$fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty() - MultilineLambdaItParameter:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2${ Timber.w(it, "Unable to get pairs") throw it } - MultilineLambdaItParameter:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2${ it.currency.getContractAddress() == pair.from.contractAddress && it.currency.network.backendId == pair.from.network } - MultilineLambdaItParameter:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2${ it.currency.getContractAddress() == pair.to.contractAddress && it.currency.network.backendId == pair.to.network } - MultilineLambdaItParameter:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2${ it.getContractAddress() == pair.from.contractAddress && it.network.backendId == pair.from.network } - MultilineLambdaItParameter:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2${ it.getContractAddress() == pair.to.contractAddress && it.network.backendId == pair.to.network } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ it.checkId( checkUserWalletId = userWalletId, fromCurrencyId = fromCryptoCurrency.id, toCurrencyId = toCryptoCurrency.id, ) } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ it.userWalletId == userWallet.walletId.stringValue && ( it.toCryptoCurrencyId == cryptoCurrencyId.value || it.fromCryptoCurrencyId == cryptoCurrencyId.value ) } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ listConverter.convertBack( value = it, multiAccountList = multiAccountList, userWallet = userWallet, txStatuses = txStatuses, ) } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ storeTransactionState( txId = transaction.txId, status = it, accountWithCurrency = fromAccount?.accountId to fromCryptoCurrency, ) } - MultilineLambdaItParameter:SwapDataConverter.kt$SwapDataConverter${ if (it == "0") { BigDecimal.ZERO } else { requireNotNull(it.toBigDecimalOrNull()) { "wrong amount format, use only digits" } } } - NoNameShadowing:DefaultSwapRepositoryV2.kt$DefaultSwapRepositoryV2$mappedProviders - NoNameShadowing:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ it.txId == txId } - - diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index 343ba4d4bd..94583d0bee 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -79,23 +79,23 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( userWallet = userWallet, filterProviderTypes = filterProviderTypes, ) - val mappedProviders = providers.associateBy(ExpressProvider::providerId) + val expressProviders = providers.associateBy(ExpressProvider::providerId) allPairs.map { pair -> async { val statusFrom = cryptoCurrencyStatusList - .firstOrNull { - it.currency.getContractAddress() == pair.from.contractAddress && - it.currency.network.backendId == pair.from.network + .firstOrNull { currencyStatus -> + currencyStatus.currency.getContractAddress() == pair.from.contractAddress && + currencyStatus.currency.network.backendId == pair.from.network } val statusTo = cryptoCurrencyStatusList - .firstOrNull { - it.currency.getContractAddress() == pair.to.contractAddress && - it.currency.network.backendId == pair.to.network + .firstOrNull { currencyStatus -> + currencyStatus.currency.getContractAddress() == pair.to.contractAddress && + currencyStatus.currency.network.backendId == pair.to.network } val mappedProviders = pair.providers.mapNotNull { - mappedProviders[it.providerId] + expressProviders[it.providerId] }.filterYieldSupplyProvider(statusFrom) if (statusFrom != null && statusTo != null && mappedProviders.isNotEmpty()) { @@ -135,16 +135,16 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( async { val statusFromDeferred = async { cryptoCurrencyList - .firstOrNull { - it.getContractAddress() == pair.from.contractAddress && - it.network.backendId == pair.from.network + .firstOrNull { currency -> + currency.getContractAddress() == pair.from.contractAddress && + currency.network.backendId == pair.from.network } } val statusToDeferred = async { cryptoCurrencyList - .firstOrNull { - it.getContractAddress() == pair.to.contractAddress && - it.network.backendId == pair.to.network + .firstOrNull { currency -> + currency.getContractAddress() == pair.to.contractAddress && + currency.network.backendId == pair.to.network } } @@ -213,27 +213,27 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( expressOperationType: ExpressOperationType, ): SwapDataModel = withContext(coroutineDispatcher.io) { val requestId = UUID.randomUUID().toString() - val fromCryptoCurrency = fromCryptoCurrencyStatus.currency + val (fromCurrency, fromStatus) = fromCryptoCurrencyStatus val refundData = when (expressProvider.type) { ExpressProviderType.CEX, ExpressProviderType.DEX_BRIDGE, ExpressProviderType.DEX, -> SwapRefundData( - refundAddress = fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value, + refundAddress = fromStatus.networkAddress?.defaultAddress?.value, refundExtraId = null, // currently always null ) else -> null } val response = tangemExpressApi.getExchangeData( - fromContractAddress = fromCryptoCurrency.getContractAddress(), + fromContractAddress = fromCurrency.getContractAddress(), toContractAddress = toCryptoCurrency.getContractAddress(), - fromNetwork = fromCryptoCurrency.network.backendId, + fromNetwork = fromCurrency.network.backendId, toNetwork = toCryptoCurrency.network.backendId, - fromAddress = fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(), + fromAddress = fromStatus.networkAddress?.defaultAddress?.value.orEmpty(), toAddress = toAddress, - fromDecimals = fromCryptoCurrency.decimals, + fromDecimals = fromCurrency.decimals, toDecimals = toCryptoCurrency.decimals, fromAmount = fromAmount, providerId = expressProvider.providerId, @@ -277,6 +277,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( txHash: String, txExtraId: String?, ) { + val (currency, status) = fromCryptoCurrencyStatus withContext(coroutineDispatcher.io) { tangemExpressApi.exchangeSent( userWalletId = userWallet.walletId.stringValue, @@ -286,8 +287,8 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( ), body = ExchangeSentRequestBody( txId = txId, - fromNetwork = fromCryptoCurrencyStatus.currency.network.backendId, - fromAddress = fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(), + fromNetwork = currency.network.backendId, + fromAddress = status.networkAddress?.defaultAddress?.value.orEmpty(), payinAddress = toAddress, payinExtraId = txExtraId, txHash = txHash, @@ -364,9 +365,9 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( ), ).getOrThrow() }, - onError = { - Timber.w(it, "Unable to get pairs") - throw it + onError = { error -> + Timber.w(error, "Unable to get pairs") + throw error }, ) } @@ -403,7 +404,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( value = NetworkStatus.MissedDerivation, // Caution!!! Do not change this status ).some(), maybeQuoteStatus = quoteStatus.toOption(), - maybeYieldBalance = none(), + maybeStakingBalance = none(), ) } diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt index 0e2d2a1a79..cbcb56f715 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt @@ -64,10 +64,10 @@ internal class DefaultSwapTransactionRepository( toAccount: Account.CryptoPortfolio?, transaction: SwapTransactionModel, ) { - transaction.status?.let { + transaction.status?.let { swapTxList -> storeTransactionState( txId = transaction.txId, - status = it, + status = swapTxList, accountWithCurrency = fromAccount?.accountId to fromCryptoCurrency, ) } @@ -76,8 +76,8 @@ internal class DefaultSwapTransactionRepository( key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, ) val tokenTransactions = savedTransactions - ?.firstOrNull { - it.checkId( + ?.firstOrNull { swapTxList -> + swapTxList.checkId( checkUserWalletId = userWalletId, fromCurrencyId = fromCryptoCurrency.id, toCurrencyId = toCryptoCurrency.id, @@ -129,17 +129,17 @@ internal class DefaultSwapTransactionRepository( }, ) { savedTransactions, txStatuses, multiAccountList -> val currencyTxs = savedTransactions - ?.filter { - it.userWalletId == userWallet.walletId.stringValue && + ?.filter { swapTxList -> + swapTxList.userWalletId == userWallet.walletId.stringValue && ( - it.toCryptoCurrencyId == cryptoCurrencyId.value || - it.fromCryptoCurrencyId == cryptoCurrencyId.value + swapTxList.toCryptoCurrencyId == cryptoCurrencyId.value || + swapTxList.fromCryptoCurrencyId == cryptoCurrencyId.value ) } - currencyTxs?.mapNotNull { + currencyTxs?.mapNotNull { swapTxList -> listConverter.convertBack( - value = it, + value = swapTxList, multiAccountList = multiAccountList, userWallet = userWallet, txStatuses = txStatuses, @@ -155,8 +155,8 @@ internal class DefaultSwapTransactionRepository( ) val tokenTransactions = savedList ?.asSequence() - ?.map { - it.copy(transactions = it.transactions.filterNot { it.txId == txId }) + ?.map { swapTxList -> + swapTxList.copy(transactions = swapTxList.transactions.filterNot { swapTx -> swapTx.txId == txId }) }?.filterNot { it.transactions.isEmpty() } ?.toList() @@ -257,8 +257,8 @@ internal class DefaultSwapTransactionRepository( toAccount = toAccount, tokenTransactions = transactions, ), - predicate = { - it.checkId( + predicate = { swapTxList -> + swapTxList.checkId( checkUserWalletId = userWalletId, fromCurrencyId = fromCryptoCurrency.id, toCurrencyId = toCryptoCurrency.id, diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/SwapDataConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/SwapDataConverter.kt index 34f26aed27..0bac85ac2e 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/converter/SwapDataConverter.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/SwapDataConverter.kt @@ -31,11 +31,11 @@ internal class SwapDataConverter : Converter + if (otherFee == "0") { BigDecimal.ZERO } else { - requireNotNull(it.toBigDecimalOrNull()) { "wrong amount format, use only digits" } + requireNotNull(otherFee.toBigDecimalOrNull()) { "wrong amount format, use only digits" } } } SwapDataTransactionModel.DEX( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt index de056fb431..336256a2de 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt @@ -9,7 +9,7 @@ import com.tangem.data.tokens.converters.UtxoConverter import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.utils.getTotalStakingBalance import com.tangem.domain.tokens.model.CurrencyAmount @@ -136,7 +136,7 @@ internal class DefaultCurrencyChecksRepository( val rentData = walletManagersFacade.getRentInfo(userWalletId, currencyStatus.currency.network) ?: return null val balanceValue = currencyStatus.value as? CryptoCurrencyStatus.Loaded ?: return null - val stakingBalance = balanceValue.yieldBalance as? YieldBalance.Data + val stakingBalance = balanceValue.stakingBalance as? StakingBalance.Data val stakingTotalBalance = stakingBalance?.getTotalStakingBalance( blockchainId = currencyStatus.currency.network.rawId, ).orZero() diff --git a/data/transaction/detekt-baseline-debug.xml b/data/transaction/detekt-baseline-debug.xml deleted file mode 100644 index dd2976b413..0000000000 --- a/data/transaction/detekt-baseline-debug.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - CastNullableToNonNullableType:DefaultTransactionRepository.kt$DefaultTransactionRepository$as - NoNameShadowing:DefaultTransactionRepository.kt$DefaultTransactionRepository$amount - NoNameShadowing:DefaultTransactionRepository.kt$DefaultTransactionRepository$destination - NullableBooleanCheck:DefaultWalletAddressServiceRepository.kt$DefaultWalletAddressServiceRepository$(walletManager as? NearWalletManager)?.validateAddress(address) ?: false - NullableToStringCall:DefaultTransactionRepository.kt$DefaultTransactionRepository$${walletManager?.wallet?.blockchain} - - diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index 26cb2d893e..f7c4ffff50 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -38,7 +38,7 @@ import timber.log.Timber import java.math.BigDecimal import java.math.BigInteger -@Suppress("LargeClass") +@Suppress("LargeClass", "NullableToStringCall") internal class DefaultTransactionRepository( private val tangemTechApi: TangemTechApi, private val walletManagersFacade: WalletManagersFacade, @@ -64,12 +64,12 @@ internal class DefaultTransactionRepository( val extras = txExtras ?: getMemoExtras(networkId = network.rawId, memo) - val destination = if (amount.type is AmountType.TokenYieldSupply) { + val patchedDestination = if (amount.type is AmountType.TokenYieldSupply) { walletManager.getYieldModuleAddress() } else { destination } - val amount = if (amount.type is AmountType.TokenYieldSupply) { + val patchedAmount = if (amount.type is AmountType.TokenYieldSupply) { amount.copy(value = BigDecimal.ZERO) } else { amount @@ -77,17 +77,17 @@ internal class DefaultTransactionRepository( return@withContext if (fee != null) { walletManager.createTransaction( - amount = amount, + amount = patchedAmount, fee = fee, - destination = destination, + destination = patchedDestination, ).copy( extras = extras, ) } else { TransactionData.Uncompiled( - amount = amount, + amount = patchedAmount, sourceAddress = walletManager.wallet.address, - destinationAddress = destination, + destinationAddress = patchedDestination, extras = extras, fee = null, ) @@ -288,7 +288,7 @@ internal class DefaultTransactionRepository( blockchain = blockchain, derivationPath = network.derivationPath.value, ) - (walletManager as TransactionSender).send(txData, signer) + (requireNotNull(walletManager) as TransactionSender).send(txData, signer) } override suspend fun sendMultipleTransactions( @@ -304,7 +304,7 @@ internal class DefaultTransactionRepository( blockchain = blockchain, derivationPath = network.derivationPath.value, ) - (walletManager as TransactionSender).sendMultiple(txsData, signer, sendMode) + (requireNotNull(walletManager) as TransactionSender).sendMultiple(txsData, signer, sendMode) } override fun createTransactionDataExtras( diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt index 23990f9704..49244f2da1 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt @@ -89,7 +89,7 @@ class DefaultWalletAddressServiceRepository( blockchain = blockchain, derivationPath = network.derivationPath.value, ) ?: return@withContext false - (walletManager as? NearWalletManager)?.validateAddress(address) ?: false + (walletManager as? NearWalletManager)?.validateAddress(address) == true } else { blockchain.validateAddress(address) } diff --git a/data/visa/detekt-baseline-debug.xml b/data/visa/detekt-baseline-debug.xml index bf83464b1c..d9da47d03d 100644 --- a/data/visa/detekt-baseline-debug.xml +++ b/data/visa/detekt-baseline-debug.xml @@ -14,10 +14,8 @@ NullCheckOnMutableProperty:VisaLibLoader.kt$VisaLibLoader$if (config != null) return@withLock requireNotNull(config) NullCheckOnMutableProperty:VisaLibLoader.kt$VisaLibLoader$if (provider != null) return@withLock requireNotNull(provider) NullableToStringCall:DefaultOnboardingRepository.kt$DefaultOnboardingRepository$${error.message} - NullableToStringCall:TangemPayRequestPerformer.kt$TangemPayRequestPerformer$${error.message} RedundantSuspendModifier:DefaultVisaRepository.kt$DefaultVisaRepository$suspend SuspendFunSwallowedCancellation:DefaultVisaRepository.kt$DefaultVisaRepository$runCatching - SuspendFunSwallowedCancellation:TangemPayRequestPerformer.kt$TangemPayRequestPerformer$runCatching SuspendFunSwallowedCancellation:VisaApiRequestMaker.kt$VisaApiRequestMaker$runCatching UnreachableCode:VisaApiRequestMaker.kt$VisaApiRequestMaker$if (status is VisaCardActivationStatus.RefreshTokenExpired) { throw RefreshTokenExpiredException() } UnreachableCode:VisaApiRequestMaker.kt$VisaApiRequestMaker$return (status as? VisaCardActivationStatus.Activated)?.visaAuthTokens ?: error("Visa card is not activated") diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt index 1dafcfd572..fc619e65ab 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt @@ -279,6 +279,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( ApiEnvironment.DEV_2, ApiEnvironment.DEV_3, ApiEnvironment.STAGE, + ApiEnvironment.STAGE_2, ApiEnvironment.MOCK, -> visaLibLoader.getOrCreateConfig().rainRSAPublicKey.dev ApiEnvironment.PROD -> visaLibLoader.getOrCreateConfig().rainRSAPublicKey.prod diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultGetTangemPayCurrencyStatusUseCase.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultGetTangemPayCurrencyStatusUseCase.kt index 53ca506151..eadc740e60 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultGetTangemPayCurrencyStatusUseCase.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultGetTangemPayCurrencyStatusUseCase.kt @@ -53,7 +53,7 @@ internal class DefaultGetTangemPayCurrencyStatusUseCase @Inject constructor( ), sources = CryptoCurrencyStatus.Sources(), pendingTransactions = emptySet(), - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, ), diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt index a227353e33..5b5ee71abe 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt @@ -171,6 +171,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( ApiEnvironment.DEV_2, ApiEnvironment.DEV_3, ApiEnvironment.STAGE, + ApiEnvironment.STAGE_2, ApiEnvironment.MOCK, -> rsaPublicKey.dev ApiEnvironment.PROD -> rsaPublicKey.prod diff --git a/data/wallet-connect/detekt-baseline-debug.xml b/data/wallet-connect/detekt-baseline-debug.xml index 6d06073a70..f357361c71 100644 --- a/data/wallet-connect/detekt-baseline-debug.xml +++ b/data/wallet-connect/detekt-baseline-debug.xml @@ -47,7 +47,6 @@ SuspendFunSwallowedCancellation:DefaultWcPairUseCase.kt$DefaultWcPairUseCase$runCatching UseAnyOrNoneInsteadOfFind:DefaultWcSessionsManager.kt$DefaultWcSessionsManager$find { it.sdkModel.topic == dto.topic } UseEmptyCounterpart:AssociateNetworksDelegate.kt$AssociateNetworksDelegate.Companion$listOf() - UseEmptyCounterpart:DefaultWcRequestService.kt$DefaultWcRequestService$setOf() UseEmptyCounterpart:WcAppMetaDataConverter.kt$WcAppMetaDataConverter$listOf() UseEmptyCounterpart:WcNetworksConverter.kt$WcNetworksConverter$listOf() UseEmptyCounterpart:WcSdkSessionConverter.kt$WcSdkSessionConverter$listOf() diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index d060e85384..f07bf2bd9d 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -166,7 +166,6 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( } override fun approve(sessionForApprove: WcSessionApprove) { - analytics.send(WcAnalyticEvents.PairButtonConnect) onCallTerminalAction.trySend(TerminalAction.Approve(sessionForApprove)) } diff --git a/data/wallet-manager/detekt-baseline-debug.xml b/data/wallet-manager/detekt-baseline-debug.xml index b5ce5d471d..967025b8b6 100644 --- a/data/wallet-manager/detekt-baseline-debug.xml +++ b/data/wallet-manager/detekt-baseline-debug.xml @@ -2,10 +2,7 @@ - MultilineLambdaItParameter:DefaultWalletManagersFacade.kt$DefaultWalletManagersFacade${ Token( name = it.name, symbol = it.symbol, contractAddress = it.contractAddress, decimals = it.decimals, id = it.id, ) } MultilineLambdaItParameter:UpdateWalletManagerResultFactory.kt$UpdateWalletManagerResultFactory${ createCurrencyTransaction( txHistoryItemConverter = txHistoryItemConverter, data = it, ) } - NamedArguments:DefaultWalletManagersFacade.kt$DefaultWalletManagersFacade$getAndUpdateWalletManager(userWallet, blockchain, derivationPath, extraTokens) - UnnecessaryLet:DefaultWalletManagersFacade.kt$DefaultWalletManagersFacade$let(txHistoryStateConverter::convert) UnsafeCallOnNullableType:WalletManagerFactory.kt$blockchain.getTestnetVersion()!! UnsafeCallOnNullableType:WalletManagerFactory.kt$scanResponse.secondTwinPublicKey!! diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt index 5b0f50b11d..7e669ccebb 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt @@ -85,7 +85,12 @@ internal class DefaultWalletManagersFacade @Inject constructor( val blockchain = network.toBlockchain() val derivationPath = network.derivationPath.value - return getAndUpdateWalletManager(userWallet, blockchain, derivationPath, extraTokens) + return getAndUpdateWalletManager( + userWallet = userWallet, + blockchain = blockchain, + derivationPath = derivationPath, + extraTokens = extraTokens, + ) } override suspend fun remove(userWalletId: UserWalletId, networks: Set) { @@ -123,18 +128,18 @@ internal class DefaultWalletManagersFacade @Inject constructor( if (tokenInfos.isEmpty()) return tokenInfos - .groupBy { it.network } + .groupBy(TokenInfo::network) .forEach { (network, tokenInfoList) -> removeTokens( userWalletId = userWalletId, network = network, - networkTokens = tokenInfoList.map { + networkTokens = tokenInfoList.map { tokenInfo -> Token( - name = it.name, - symbol = it.symbol, - contractAddress = it.contractAddress, - decimals = it.decimals, - id = it.id, + name = tokenInfo.name, + symbol = tokenInfo.symbol, + contractAddress = tokenInfo.contractAddress, + decimals = tokenInfo.decimals, + id = tokenInfo.id, ) }, ) @@ -215,24 +220,24 @@ internal class DefaultWalletManagersFacade @Inject constructor( "Unable to get a wallet manager for blockchain: ${currency.network}" } - return walletManager - .getTransactionHistoryState( - address = walletManager.wallet.address, - filterType = when (currency) { - is CryptoCurrency.Coin -> TransactionHistoryRequest.FilterType.Coin - is CryptoCurrency.Token -> { - val blockchainToken = Token( - name = currency.name, - symbol = currency.symbol, - contractAddress = currency.contractAddress, - decimals = currency.decimals, - id = currency.id.rawCurrencyId?.value, - ) - TransactionHistoryRequest.FilterType.Contract(blockchainToken) - } - }, - ) - .let(txHistoryStateConverter::convert) + val transactionHistoryState = walletManager.getTransactionHistoryState( + address = walletManager.wallet.address, + filterType = when (currency) { + is CryptoCurrency.Coin -> TransactionHistoryRequest.FilterType.Coin + is CryptoCurrency.Token -> { + val blockchainToken = Token( + name = currency.name, + symbol = currency.symbol, + contractAddress = currency.contractAddress, + decimals = currency.decimals, + id = currency.id.rawCurrencyId?.value, + ) + TransactionHistoryRequest.FilterType.Contract(blockchainToken) + } + }, + ) + + return txHistoryStateConverter.convert(transactionHistoryState) } override suspend fun getTxHistoryItems( @@ -366,7 +371,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( blockchain: Blockchain, derivationPath: String?, ): WalletManager? { - getWmInitializationMutex(blockchain, derivationPath).withLock { + getWmInitializationMutex(userWalletId, blockchain, derivationPath).withLock { val userWallet = getUserWallet(userWalletId) var walletManager = walletManagersStore.getSyncOrNull( @@ -738,15 +743,24 @@ internal class DefaultWalletManagersFacade @Inject constructor( return initializableAccountWalletManger.accountInitializationState == InitializableAccount.State.INITIALIZED } - private fun getWmInitializationMutex(blockchain: Blockchain, derivationPath: String?): Mutex { - val key = createMutexMapKey(blockchain, derivationPath) + private fun getWmInitializationMutex( + userWalletId: UserWalletId, + blockchain: Blockchain, + derivationPath: String?, + ): Mutex { + val key = createMutexMapKey(userWalletId, blockchain, derivationPath) return wmInitializationMutexes.computeIfAbsent(key) { Mutex() } } - private fun createMutexMapKey(blockchain: Blockchain, derivationPath: String?): String { - return blockchain.toNetworkId() + "|" + derivationPath + private fun createMutexMapKey(userWalletId: UserWalletId, blockchain: Blockchain, derivationPath: String?): String { + return listOf( + userWalletId.stringValue, + blockchain.toNetworkId(), + derivationPath, + ) + .joinToString(separator = "|") } private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set) { diff --git a/data/wallets/build.gradle.kts b/data/wallets/build.gradle.kts index f7867f6b5d..5949dd0174 100644 --- a/data/wallets/build.gradle.kts +++ b/data/wallets/build.gradle.kts @@ -25,13 +25,12 @@ dependencies { implementation(projects.core.utils) /** Domain */ - implementation(projects.domain.wallets) + implementation(projects.domain.account) implementation(projects.domain.card) - api(projects.domain.models) - - /** Domain models */ - implementation(projects.domain.wallets.models) + implementation(projects.domain.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) /** DI */ implementation(deps.hilt.android) @@ -41,15 +40,15 @@ dependencies { implementation(deps.androidx.datastore) implementation(deps.arrow.core) implementation(deps.kotlin.coroutines) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) + implementation(deps.retrofit) implementation(deps.timber) /** tests */ - testImplementation(projects.domain.models) testImplementation(projects.common.test) testImplementation(deps.test.junit) testImplementation(deps.test.coroutine) testImplementation(deps.test.truth) testImplementation(deps.test.mockk) - testImplementation(deps.moshi) - testImplementation(deps.moshi.kotlin) } \ No newline at end of file diff --git a/data/wallets/detekt-baseline-debug.xml b/data/wallets/detekt-baseline-debug.xml index 7ac40bc693..d8e411e670 100644 --- a/data/wallets/detekt-baseline-debug.xml +++ b/data/wallets/detekt-baseline-debug.xml @@ -11,7 +11,6 @@ MultilineLambdaItParameter:TangemHotWalletSigner.kt$TangemHotWalletSigner${ Timber.e(it) return if (it is TangemSdkError) { CompletionResult.Failure(it) } else { CompletionResult.Failure(TangemSdkError.ExceptionError(it)) } } NamedArguments:DefaultHotWalletAccessCodeAttemptsRepository.kt$DefaultHotWalletAccessCodeAttemptsRepository$toState(id, count, deadline, boot) NamedArguments:DefaultHotWalletAccessCodeAttemptsRepository.kt$DefaultHotWalletAccessCodeAttemptsRepository$toState(id, it.attempts, it.deadline, it.bootCount) - SuspendFunSwallowedCancellation:DefaultHotWalletAccessor.kt$DefaultHotWalletAccessor$runCatching SuspendFunSwallowedCancellation:TangemHotWalletSigner.kt$TangemHotWalletSigner$runCatching UnnecessaryLet:MissedDerivationsFinder.kt$MissedDerivationsFinder$let(::findByNetworks) UseOrEmpty:DefaultColdMapDerivationsRepository.kt$DefaultColdMapDerivationsRepository$oldKeys[walletKey] ?: emptyMap() diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index ba985743b4..797d06f372 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -5,22 +5,22 @@ import arrow.core.left import arrow.core.right import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter import com.tangem.datasource.api.common.AuthProvider +import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError.HttpException import com.tangem.datasource.api.common.response.fold import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.common.response.isNetworkError import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter -import com.tangem.datasource.api.tangemTech.models.PromocodeActivationBody -import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO +import com.tangem.datasource.api.tangemTech.models.* import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status -import com.tangem.datasource.api.tangemTech.models.WalletBody -import com.tangem.datasource.api.tangemTech.models.WalletType import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.PreferencesKeys.SEED_FIRST_NOTIFICATION_SHOW_TIME import com.tangem.datasource.local.preferences.utils.* import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus @@ -30,13 +30,15 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.WEEK_MILLIS import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext typealias SeedPhraseNotificationsStatuses = Map -@Suppress("TooManyFunctions", "LargeClass") +@Suppress("TooManyFunctions", "LargeClass", "LongParameterList") internal class DefaultWalletsRepository( private val appPreferencesStore: AppPreferencesStore, private val tangemTechApi: TangemTechApi, @@ -44,6 +46,8 @@ internal class DefaultWalletsRepository( private val seedPhraseNotificationVisibilityStore: RuntimeStateStore, private val dispatchers: CoroutineDispatcherProvider, private val authProvider: AuthProvider, + private val accountsFeatureToggles: AccountsFeatureToggles, + private val moshi: com.squareup.moshi.Moshi, ) : WalletsRepository { private val upgradeWalletNotificationDisabled: MutableStateFlow> = @@ -344,18 +348,27 @@ internal class DefaultWalletsRepository( upgradeWalletNotificationDisabled.update { it.plus(userWalletId) } } - override suspend fun setWalletName(walletId: String, walletName: String) = withContext(dispatchers.io) { - val userWallet = userWalletsStore.getSyncOrNull(key = UserWalletId(walletId)) + override suspend fun setWalletName(walletId: UserWalletId, walletName: String) = withContext(dispatchers.io) { + val userWallet = userWalletsStore.getSyncOrNull(key = walletId) tangemTechApi.updateWallet( - walletId = walletId, + walletId = walletId.stringValue, body = WalletBody(name = walletName, type = WalletType.from(userWallet)), ).getOrThrow() } - override suspend fun getWalletInfo(walletId: String): UserWalletRemoteInfo = withContext(dispatchers.io) { + override suspend fun upgradeWallet(walletId: UserWalletId) = withContext(dispatchers.io) { + val userWallet = userWalletsStore.getSyncStrict(key = walletId) + + tangemTechApi.updateWallet( + walletId = walletId.stringValue, + body = WalletBody(name = userWallet.name, type = WalletType.from(userWallet)), + ).getOrThrow() + } + + override suspend fun getWalletInfo(walletId: UserWalletId): UserWalletRemoteInfo = withContext(dispatchers.io) { UserWalletRemoteInfoConverter.convert( - value = tangemTechApi.getWalletById(walletId).getOrThrow(), + value = tangemTechApi.getWalletById(walletId.stringValue).getOrThrow(), ) } @@ -379,24 +392,58 @@ internal class DefaultWalletsRepository( override suspend fun associateWallets(applicationId: String, wallets: List) = withContext(dispatchers.io) { - val publicKeys = authProvider.getCardsPublicKeys() - val walletsBody = wallets.map { userWallet -> - WalletIdBodyConverter.convert( - userWallet = userWallet, - publicKeys = if (userWallet is UserWallet.Cold) { - publicKeys.filterKeys { - userWallet.cardsInWallet.contains(it) - } - } else { - emptyMap() - }, - ) - } + if (accountsFeatureToggles.isFeatureEnabled) { + val associateApplicationIdWithWallets: suspend () -> ApiResponse = { + tangemTechApi.associateApplicationIdWithWalletsV2( + applicationId = applicationId, + body = AssociateApplicationIdWithWalletsBody( + walletIds = wallets.map { it.walletId.stringValue }.distinct(), + ), + ) + } - tangemTechApi.associateApplicationIdWithWallets( - applicationId = applicationId, - body = walletsBody, - ).getOrThrow() + val apiResponse = associateApplicationIdWithWallets() + + if (apiResponse is ApiResponse.Success) return@withContext + + if (apiResponse is ApiResponse.Error && + apiResponse.cause.isNetworkError(HttpException.Code.BAD_REQUEST) + ) { + val errorBody = (apiResponse.cause as? HttpException)?.errorBody + ?: error("Bad Request must have error body") + + val adapter = moshi.adapter(AssociateAppWithWalletsErrorResponse::class.java) + val errorResponse = adapter.fromJson(errorBody) + ?: error("Cannot parse error body: $errorBody") + + errorResponse.missingWalletIds + .map { + async { createWallet(userWalletId = UserWalletId(it)) } + } + .awaitAll() + + associateApplicationIdWithWallets().getOrThrow() + } + } else { + val publicKeys = authProvider.getCardsPublicKeys() + val walletsBody = wallets.map { userWallet -> + WalletIdBodyConverter.convert( + userWallet = userWallet, + publicKeys = if (userWallet is UserWallet.Cold) { + publicKeys.filterKeys { + userWallet.cardsInWallet.contains(it) + } + } else { + emptyMap() + }, + ) + } + + tangemTechApi.associateApplicationIdWithWallets( + applicationId = applicationId, + body = walletsBody, + ).getOrThrow() + } } override suspend fun activatePromoCode( diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt index bcaff10b6f..a1e870a3e1 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt @@ -1,5 +1,6 @@ package com.tangem.data.wallets.di +import com.squareup.moshi.Moshi import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository import com.tangem.data.wallets.DefaultWalletsRepository import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository @@ -8,9 +9,11 @@ import com.tangem.data.wallets.hot.DefaultHotMapDerivationsRepository import com.tangem.data.wallets.hot.DefaultHotWalletAccessCodeAttemptsRepository import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository @@ -37,6 +40,8 @@ internal object WalletsDataModule { userWalletsStore: UserWalletsStore, dispatchers: CoroutineDispatcherProvider, authProvider: AuthProvider, + accountsFeatureToggles: AccountsFeatureToggles, + @NetworkMoshi moshi: Moshi, ): WalletsRepository { return DefaultWalletsRepository( appPreferencesStore = appPreferencesStore, @@ -45,6 +50,8 @@ internal object WalletsDataModule { seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()), dispatchers = dispatchers, authProvider = authProvider, + accountsFeatureToggles = accountsFeatureToggles, + moshi = moshi, ) } diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt index 00817f74de..f829c76c6f 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessor.kt @@ -11,6 +11,7 @@ import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.exception.WrongPasswordException import com.tangem.hot.sdk.model.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch @@ -119,25 +120,27 @@ class DefaultHotWalletAccessor @Inject constructor( originalAuth = auth, auth = auth, block = { blockAuth -> - block(blockAuth).also { - // Update biometry auth if the original auth was password - updateBiometryAuthIfNeeded( - hotWalletId = hotWalletId, - originalAuth = blockAuth, - ) - } + val result = block(blockAuth) + + // Update biometry auth if the original auth was password + updateBiometryAuthIfNeeded( + hotWalletId = hotWalletId, + originalAuth = auth, + ) + + result }, ) } private suspend fun updateBiometryAuthIfNeeded(hotWalletId: HotWalletId, originalAuth: HotAuth) { val isAccessCodeRequired = walletsRepository.requireAccessCode() + val isUseBiometricAuthenticationEnabled = walletsRepository.useBiometricAuthentication() - if (originalAuth is HotAuth.Password && isAccessCodeRequired.not()) { + if (originalAuth is HotAuth.Password && isUseBiometricAuthenticationEnabled && isAccessCodeRequired.not()) { val userWallet = userWalletsListRepository.userWalletsSync() - .find { it is UserWallet.Hot && it.hotWalletId == hotWalletId } - as? UserWallet.Hot - ?: return + .first { it is UserWallet.Hot && it.hotWalletId == hotWalletId } + as UserWallet.Hot val newHotWalletId = tangemHotSdk.changeAuth( unlockHotWallet = UnlockHotWallet( @@ -161,7 +164,7 @@ class DefaultHotWalletAccessor @Inject constructor( originalAuth: HotAuth, auth: HotAuth, block: suspend (auth: HotAuth) -> T, - ): T = runCatching { + ): T = runSuspendCatching { block(auth) }.getOrElse { exception -> if (auth is HotAuth.Biometry && exception.isBiometryError()) { diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt index e2a9090de4..80b3e8d659 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt @@ -13,6 +13,7 @@ import com.tangem.datasource.api.tangemTech.models.PromocodeActivationResponse import com.tangem.datasource.api.tangemTech.models.WalletResponse import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError @@ -51,6 +52,8 @@ class DefaultWalletsRepositoryTest { seedPhraseNotificationVisibilityStore = mockk(), dispatchers = dispatchers, authProvider = mockk(), + accountsFeatureToggles = mockk(), + moshi = mockk(), ) } @@ -204,6 +207,10 @@ class DefaultWalletsRepositoryTest { coEvery { getCardsPublicKeys() } returns publicKeys } + val accountsFeatureToggles = mockk { + every { isFeatureEnabled } returns false + } + repository = DefaultWalletsRepository( appPreferencesStore = appPreferenceStore, tangemTechApi = tangemTechApi, @@ -211,6 +218,8 @@ class DefaultWalletsRepositoryTest { seedPhraseNotificationVisibilityStore = mockk(), dispatchers = dispatchers, authProvider = authProvider, + accountsFeatureToggles = accountsFeatureToggles, + moshi = mockk(), ) coEvery { diff --git a/detekt_baseline_report.txt b/detekt_baseline_report.txt index 85ca6a00e5..a45e2bff61 100644 --- a/detekt_baseline_report.txt +++ b/detekt_baseline_report.txt @@ -1,13 +1,13 @@ ========================================== Detekt Baseline Updater & Issue Counter ========================================== -Date: 2025-12-02 13:19:49 +Date: 2025-12-12 13:17:20 Step 1: Running detekt to check for new issues... ✓ Detekt passed - no new issues found -Step 2: Updating detekt baseline for debug variant... +Step 2: Updating detekt baseline ... Baseline updated successfully! @@ -17,13 +17,13 @@ Counting issues in baseline files... ========================================== Summary: - Total Issues: 1435 - Modules with Issues: 62 - Average Issues per Module: 23 + Total Issues: 1378 + Modules with Issues: 72 + Average Issues per Module: 19 Progress: - Fixed: 367 out of 1802 (20%) - Remaining: 1435 + Fixed: 555 out of 1933 (28%) + Remaining: 1378 ========================================== All Modules with Issues (sorted by count) @@ -31,66 +31,76 @@ All Modules with Issues (sorted by count) Module Issues ──────────────────────────────────────────────────────────────── -features/wallet/impl 148 -features/markets/impl 148 -features/onboarding-v2/impl 130 +features/markets/impl 147 +features/wallet/impl 144 +features/onboarding-v2/impl 128 features/swap/impl 67 -features/send-v2/impl 57 -data/wallet-connect 55 -features/hot-wallet/impl 53 -features/tokendetails/impl 48 -features/staking/impl 48 +data/wallet-connect 54 +features/hot-wallet/impl 51 features/walletconnect/impl 47 -features/manage-tokens/impl 45 -features/swap-v2/impl 40 +features/staking/impl 46 +features/tokendetails/impl 45 +features/manage-tokens/impl 44 domain/wallets 37 features/nft/impl 34 -features/tester/impl 31 -domain/tokens 28 -core/ui 27 -common/ui 26 +features/tester/impl 28 +core/ui 26 features/swap/domain 25 -data/visa 22 +domain/tokens 25 +common/ui 24 features/yield-supply/impl 21 features/tangempay/details/impl 21 -data/nft 20 +domain/models 21 +data/visa 21 +data/nft 19 +core/pagination 16 features/swap/data 15 -data/wallets 14 -data/swap 13 +domain/staking/models 13 +data/wallets 13 features/token-recieve/impl 11 features/qr-scanning/impl 11 -domain/account/status 11 data/onramp 11 core/datasource 11 +core/analytics/models 11 data/markets 10 features/details/impl 9 -domain/staking 9 +domain/account/status 9 data/yield-supply 9 data/networks 9 -features/referral/impl 8 +domain/visa/models 8 domain/transaction 8 +domain/staking 8 +core/utils 8 libs/tangem-sdk-api 7 +features/referral/impl 7 +domain/tokens/models 7 data/txhistory 7 features/welcome/impl 6 -data/wallet-manager 6 +domain/wallet-connect/models 6 +domain/onramp 6 +domain/account 6 libs/visa 5 -features/send-v2/api 5 features/home/impl 5 domain/markets 5 -domain/legacy 5 -data/transaction 5 +domain/core 5 data/account 5 -features/referral/domain 4 -features/biometry/impl 4 +domain/onramp/models 4 data/tokens 4 -features/txhistory/impl 3 -features/tangempay/onboarding/impl 3 -features/create-wallet-start/impl 3 -domain/manage-tokens 3 +domain/nft/models 3 +domain/balance-hiding 3 +data/wallet-manager 3 data/manage-tokens 3 -common/routing 3 -features/account/api 2 -data/promo 2 +domain/txhistory/models 2 +domain/networks 2 core/config-toggles 2 -data/feedback 1 +test/mock 1 +domain/yield-supply/models 1 +domain/wallets/models 1 +domain/transaction/models 1 +domain/quotes 1 +domain/promo 1 +domain/onboarding 1 +domain/markets/models 1 +domain/feedback/models 1 +domain/express/models 1 ──────────────────────────────────────────────────────────────── \ No newline at end of file diff --git a/domain/account/detekt-baseline-main.xml b/domain/account/detekt-baseline-main.xml new file mode 100644 index 0000000000..10f97ccab0 --- /dev/null +++ b/domain/account/detekt-baseline-main.xml @@ -0,0 +1,12 @@ + + + + + MultilineLambdaItParameter:GetArchivedAccountsUseCase.kt$GetArchivedAccountsUseCase${ send(it.lceError()) return } + MultilineLambdaItParameter:GetArchivedAccountsUseCase.kt$GetArchivedAccountsUseCase${ send(it.lceError()) return@channelFlow } + NamedArguments:AddCryptoPortfolioUseCase.kt$AddCryptoPortfolioUseCase$createAccount(userWalletId, accountName, icon, derivationIndex) + UnnecessaryAbstractClass:MultiAccountListSupplier.kt$MultiAccountListSupplier$MultiAccountListSupplier + UnnecessaryAbstractClass:SingleAccountListSupplier.kt$SingleAccountListSupplier$SingleAccountListSupplier + UnnecessaryAbstractClass:SingleAccountSupplier.kt$SingleAccountSupplier$SingleAccountSupplier + + diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt index b9378ac209..db2dc28e25 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -28,6 +28,7 @@ data class AccountList private constructor( val userWalletId: UserWalletId, val accounts: List, val totalAccounts: Int, + val totalArchivedAccounts: Int, val sortType: TokensSortType, val groupType: TokensGroupType, ) { @@ -60,6 +61,7 @@ data class AccountList private constructor( userWalletId = this.userWalletId, accounts = accounts, totalAccounts = this.totalAccounts + if (isNewAccount) 1 else 0, + totalArchivedAccounts = this.totalArchivedAccounts, sortType = this.sortType, groupType = this.groupType, ) @@ -82,6 +84,7 @@ data class AccountList private constructor( userWalletId = this.userWalletId, accounts = accounts, totalAccounts = this.totalAccounts - if (isExistingAccount) 1 else 0, + totalArchivedAccounts = this.totalArchivedAccounts, sortType = this.sortType, groupType = this.groupType, ) @@ -152,6 +155,7 @@ data class AccountList private constructor( companion object { const val MAX_ACCOUNTS_COUNT = 20 + const val MAX_ARCHIVED_ACCOUNTS_COUNT = 1000 private const val MAX_MAIN_ACCOUNTS_COUNT = 1 /** @@ -166,6 +170,7 @@ data class AccountList private constructor( userWalletId: UserWalletId, accounts: List, totalAccounts: Int, + totalArchivedAccounts: Int, sortType: TokensSortType = TokensSortType.NONE, groupType: TokensGroupType = TokensGroupType.NONE, ): Either = either { @@ -200,6 +205,7 @@ data class AccountList private constructor( userWalletId = userWalletId, accounts = accounts, totalAccounts = totalAccounts, + totalArchivedAccounts = totalArchivedAccounts, sortType = sortType, groupType = groupType, ) @@ -225,6 +231,7 @@ data class AccountList private constructor( ), ), totalAccounts = 1, + totalArchivedAccounts = 0, sortType = sortType, groupType = groupType, ) diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt index f7c9a12dc5..8309ed3403 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt @@ -26,6 +26,7 @@ data class AccountStatusList( val userWalletId: UserWalletId, val accountStatuses: List, val totalAccounts: Int, + val totalArchivedAccounts: Int, val totalFiatBalance: TotalFiatBalance, val sortType: TokensSortType, val groupType: TokensGroupType, @@ -47,6 +48,7 @@ data class AccountStatusList( userWalletId = userWalletId, accounts = accountStatuses.map(AccountStatus::account), totalAccounts = totalAccounts, + totalArchivedAccounts = totalArchivedAccounts, sortType = sortType, groupType = groupType, ) diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCase.kt index bb0714b5b1..9c7d51c56f 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCase.kt @@ -55,10 +55,11 @@ class ApplyAccountListSortingUseCase( val updatedAccountList = withError( transform = { Error.DataOperationFailed("Unable to create AccountList: $it") }, ) { - AccountList( + AccountList.invoke( userWalletId = accountList.userWalletId, accounts = sortedAccounts, totalAccounts = accountList.totalAccounts, + totalArchivedAccounts = accountList.totalArchivedAccounts, sortType = accountList.sortType, groupType = accountList.groupType, ) diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt index 34f6603024..a242546b21 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt @@ -87,6 +87,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = accounts, totalAccounts = accounts.size, + totalArchivedAccounts = 0, sortType = sortType, groupType = groupType, ) @@ -96,6 +97,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = accounts, totalAccounts = accounts.size, + totalArchivedAccounts = 0, sortType = sortType, groupType = groupType, ) @@ -110,6 +112,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = model.accounts, totalAccounts = model.totalAccounts, + totalArchivedAccounts = 0, ) // Assert @@ -206,12 +209,14 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = listOf(mainAccount), totalAccounts = 1, + totalArchivedAccounts = 0, ).getOrNull()!!, toAdd = newAccount, expected = AccountList( userWalletId = userWalletId, accounts = listOf(mainAccount, newAccount), totalAccounts = 2, + totalArchivedAccounts = 0, ), ) }, @@ -226,12 +231,14 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = listOf(mainAccount), totalAccounts = 1, + totalArchivedAccounts = 0, ).getOrNull()!!, toAdd = newAccount, expected = AccountList( userWalletId = userWalletId, accounts = listOf(newAccount), totalAccounts = 1, + totalArchivedAccounts = 0, ), ) }, @@ -252,6 +259,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = listOf(mainAccount), totalAccounts = 1, + totalArchivedAccounts = 0, ).getOrNull()!!, toAdd = newAccount, expected = AccountList.Error.MainAccountNotFound.left(), @@ -268,6 +276,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = listOf(mainAccount), totalAccounts = 1, + totalArchivedAccounts = 0, ).getOrNull()!!, toAdd = newAccount, expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(), @@ -284,6 +293,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = listOf(mainAccount), totalAccounts = 1, + totalArchivedAccounts = 0, ).getOrNull()!!, toAdd = newAccount, expected = AccountList.Error.DuplicateAccountNames.left(), @@ -296,6 +306,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = createAccounts(count = 20), totalAccounts = 20, + totalArchivedAccounts = 0, ).getOrNull()!!, toAdd = createAccount(derivationIndex = 21), expected = AccountList.Error.ExceedsMaxAccountsCount.left(), @@ -335,12 +346,14 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = listOf(mainAccount, secondaryAccount), totalAccounts = 2, + totalArchivedAccounts = 0, ).getOrNull()!!, toRemove = secondaryAccount, expected = AccountList( userWalletId = userWalletId, accounts = listOf(mainAccount), totalAccounts = 1, + totalArchivedAccounts = 0, ), ) }, @@ -354,6 +367,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = listOf(mainAccount), totalAccounts = 1, + totalArchivedAccounts = 0, ) MinusTestModel( @@ -372,6 +386,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = listOf(mainAccount), totalAccounts = 1, + totalArchivedAccounts = 0, ).getOrNull()!!, toRemove = mainAccount, expected = AccountList.Error.EmptyAccountsList.left(), @@ -388,6 +403,7 @@ internal class AccountListTest { userWalletId = userWalletId, accounts = listOf(mainAccount, secondaryAccount), totalAccounts = 2, + totalArchivedAccounts = 0, ).getOrNull()!!, toRemove = mainAccount, expected = AccountList.Error.MainAccountNotFound.left(), diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCaseTest.kt index 2d19983849..b709863d71 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ApplyAccountListSortingUseCaseTest.kt @@ -157,6 +157,7 @@ class ApplyAccountListSortingUseCaseTest { userWalletId = accountList.userWalletId, accounts = accountList.accounts.reversed(), totalAccounts = accountList.totalAccounts, + totalArchivedAccounts = accountList.totalArchivedAccounts, sortType = accountList.sortType, groupType = accountList.groupType, ).getOrNull()!! diff --git a/domain/account/status/build.gradle.kts b/domain/account/status/build.gradle.kts index 7e7cfa8521..5d47d084f1 100644 --- a/domain/account/status/build.gradle.kts +++ b/domain/account/status/build.gradle.kts @@ -23,9 +23,11 @@ dependencies { api(projects.domain.quotes) api(projects.domain.models) api(projects.domain.networks) + api(projects.domain.nft) api(projects.domain.referral) api(projects.domain.staking) api(projects.domain.tokens) + api(projects.domain.walletManager) api(projects.domain.wallets) implementation(projects.libs.blockchainSdk) diff --git a/domain/account/status/detekt-baseline-debug.xml b/domain/account/status/detekt-baseline-debug.xml index 1f66781ca0..c461b54d39 100644 --- a/domain/account/status/detekt-baseline-debug.xml +++ b/domain/account/status/detekt-baseline-debug.xml @@ -7,8 +7,6 @@ MultilineLambdaItParameter:AccountCryptoCurrencyStatusFinder.kt$AccountCryptoCurrencyStatusFinder${ val currency = it.currency val isContractAddressMatch = contractAddress == null || currency.id.contractAddress.equals(contractAddress, ignoreCase = true) currency.network.rawId == networkId.rawId.value && currency.network.derivationPath.value == derivationPath.value && isContractAddressMatch } MultilineLambdaItParameter:ApplyTokenListSortingUseCaseV2.kt$ApplyTokenListSortingUseCaseV2${ errors[account.accountId] = it return@map account } MultilineLambdaItParameter:DefaultMultiAccountStatusListProducer.kt$DefaultMultiAccountStatusListProducer${ singleAccountStatusListSupplier( params = SingleAccountStatusListProducer.Params(it.walletId), ) } - MultilineLambdaItParameter:ManageCryptoCurrenciesUseCase.kt$ManageCryptoCurrenciesUseCase${ ExpressAsset.ID( networkId = it.network.backendId, contractAddress = (it as? CryptoCurrency.Token)?.contractAddress, ) } - MultilineLambdaItParameter:ManageCryptoCurrenciesUseCase.kt$ManageCryptoCurrenciesUseCase${ it.network.backendId == networkId && !it.isCustom && it.contractAddress.equals(contractAddress, true) } UnnecessaryAbstractClass:MultiAccountStatusListSupplier.kt$MultiAccountStatusListSupplier$MultiAccountStatusListSupplier UnnecessaryAbstractClass:SingleAccountStatusListSupplier.kt$SingleAccountStatusListSupplier$SingleAccountStatusListSupplier UnnecessaryAbstractClass:SingleAccountStatusSupplier.kt$SingleAccountStatusSupplier$SingleAccountStatusSupplier diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt index 70d95e70cd..c2d429c650 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt @@ -12,10 +12,11 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.utils.NetworksCleaner 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.staking.utils.StakingCleaner import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -95,6 +96,7 @@ internal object AccountStatusUseCaseModule { accountsCRUDRepository: AccountsCRUDRepository, currenciesRepository: CurrenciesRepository, derivationsRepository: DerivationsRepository, + walletManagersFacade: WalletManagersFacade, cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher, stakingIdFactory: StakingIdFactory, networksCleaner: NetworksCleaner, @@ -107,6 +109,7 @@ internal object AccountStatusUseCaseModule { accountsCRUDRepository = accountsCRUDRepository, currenciesRepository = currenciesRepository, derivationsRepository = derivationsRepository, + walletManagersFacade = walletManagersFacade, cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher, stakingIdFactory = stakingIdFactory, networksCleaner = networksCleaner, @@ -120,18 +123,16 @@ internal object AccountStatusUseCaseModule { @Provides @Singleton fun provideCryptoCurrencyBalanceFetcher( - accountsCRUDRepository: AccountsCRUDRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + multiStakingBalanceFetcher: MultiStakingBalanceFetcher, stakingIdFactory: StakingIdFactory, dispatchers: CoroutineDispatcherProvider, ): CryptoCurrencyBalanceFetcher { return CryptoCurrencyBalanceFetcher( - accountsCRUDRepository = accountsCRUDRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, - multiYieldBalanceFetcher = multiYieldBalanceFetcher, + multiStakingBalanceFetcher = multiStakingBalanceFetcher, stakingIdFactory = stakingIdFactory, parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default), ) diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt index 8b3efcfba9..bf10f519ad 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt @@ -68,6 +68,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo accountStatuses = accountStatuses.toList(), totalAccounts = accountList.totalAccounts, totalFiatBalance = TotalFiatBalanceCalculator.calculate(balances), + totalArchivedAccounts = accountList.totalArchivedAccounts, sortType = accountList.sortType, groupType = accountList.groupType, ) @@ -194,6 +195,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo } }, totalAccounts = accountList.totalAccounts, + totalArchivedAccounts = accountList.totalArchivedAccounts, totalFiatBalance = TotalFiatBalance.Loading, sortType = accountList.sortType, groupType = accountList.groupType, diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt index a3a2854c45..f3a5fdeec0 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt @@ -106,6 +106,7 @@ class ApplyTokenListSortingUseCaseV2( userWalletId = accountList.userWalletId, accounts = accountList.accounts.sortTokens(sortedTokensIdsByAccount, errors), totalAccounts = accountList.totalAccounts, + totalArchivedAccounts = accountList.totalArchivedAccounts, sortType = if (isSortedByBalance) TokensSortType.BALANCE else TokensSortType.NONE, groupType = if (isGroupedByNetwork) TokensGroupType.NETWORK else TokensGroupType.NONE, ).getOrElse { diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt index 9277ab4837..51ffcfa1e8 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt @@ -21,8 +21,10 @@ import com.tangem.domain.networks.utils.NetworksCleaner import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.utils.StakingCleaner import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching import kotlinx.coroutines.* import timber.log.Timber @@ -49,6 +51,7 @@ class ManageCryptoCurrenciesUseCase( private val accountsCRUDRepository: AccountsCRUDRepository, private val currenciesRepository: CurrenciesRepository, private val derivationsRepository: DerivationsRepository, + private val walletManagersFacade: WalletManagersFacade, private val cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher, private val stakingIdFactory: StakingIdFactory, private val networksCleaner: NetworksCleaner, @@ -83,27 +86,19 @@ class ManageCryptoCurrenciesUseCase( val modifiedCurrencyList = accountStatus.tokenList.flattenCurrencies() .modify(add = add, remove = remove) + if (!modifiedCurrencyList.hasChanges) { + Timber.d("No changes in currencies, skipping") + return@withContext + } + saveAccount( account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet()), ) - val isDerivingFailed = derivePublicKeys( - userWalletId = userWalletId, - currencies = modifiedCurrencyList.added, - ).isLeft() + derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) parallelUpdatingScope.launch { - /* - * If only removal of currencies happened, we need to sync tokens. Otherwise, tokens will be synced - * when balances are refreshed for added currencies. - */ - val isOnlyRemoval = modifiedCurrencyList.added.isEmpty() && modifiedCurrencyList.removed.isNotEmpty() - - if (isDerivingFailed || isOnlyRemoval) { - launch { accountsCRUDRepository.syncTokens(userWalletId) } - } - - if (isDerivingFailed) return@launch + syncTokens(userWalletId, modifiedCurrencyList) cryptoCurrencyBalanceFetcher(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total) @@ -124,10 +119,10 @@ class ManageCryptoCurrenciesUseCase( val foundToken = accountStatus.tokenList.flattenCurrencies() .mapNotNull { it.currency as? CryptoCurrency.Token } - .firstOrNull { - it.network.backendId == networkId && - !it.isCustom && - it.contractAddress.equals(contractAddress, true) + .firstOrNull { token -> + token.network.backendId == networkId && + !token.isCustom && + token.contractAddress.equals(contractAddress, true) } if (foundToken != null) return@withContext foundToken @@ -140,6 +135,8 @@ class ManageCryptoCurrenciesUseCase( saveAccount(account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet())) parallelUpdatingScope.launch { + syncTokens(userWalletId, modifiedCurrencyList) + cryptoCurrencyBalanceFetcher(userWalletId = userWalletId, currencies = listOf(tokenToAdd)) refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total) } @@ -263,15 +260,40 @@ class ManageCryptoCurrenciesUseCase( ) } + private suspend fun syncTokens(userWalletId: UserWalletId, modifiedCurrencyList: ModifiedCurrencyList) { + createWalletManagers(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) + + runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) } + .onFailure { Timber.e(it, "Failed to sync tokens for wallet $userWalletId") } + } + + /** + * Creates wallet managers for the given [currencies] if they do not already exist. + * The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature. + * + * @param userWalletId The ID of the user's wallet. + * @param currencies The list of cryptocurrencies for which to create wallet managers. + */ + private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List) { + val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network) + + for (network in networks) { + runSuspendCatching { + walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network) + } + .onFailure { Timber.e(it, "Failed to create wallet manager for network ${network.id}") } + } + } + private suspend fun refreshExpress(userWalletId: UserWalletId, currencies: List) { if (currencies.isEmpty()) return coroutineScope { launch { - val assetIds = currencies.mapTo(hashSetOf()) { + val assetIds = currencies.mapTo(hashSetOf()) { currency -> ExpressAsset.ID( - networkId = it.network.backendId, - contractAddress = (it as? CryptoCurrency.Token)?.contractAddress, + networkId = currency.network.backendId, + contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress, ) } @@ -328,5 +350,8 @@ class ManageCryptoCurrenciesUseCase( val added: List, val removed: List, val total: List, - ) + ) { + + val hasChanges get() = added.isNotEmpty() || removed.isNotEmpty() + } } \ No newline at end of file diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt index d2eb7a9b1c..c972f97b80 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt @@ -7,6 +7,7 @@ import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either import arrow.core.raise.ensure +import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.account.repository.AccountsCRUDRepository @@ -23,6 +24,8 @@ import com.tangem.domain.models.wallet.UserWalletId * * @property crudRepository repository for performing CRUD operations on accounts * @property mainAccountTokensMigration handles the migration of tokens from the main account to the recovered account + * @property cryptoCurrencyBalanceFetcher Fetcher for updating crypto currency balances. + * @property singleAccountListFetcher fetches the list of accounts for a single user wallet * [REDACTED_AUTHOR] */ @@ -30,6 +33,7 @@ class RecoverCryptoPortfolioUseCase( private val crudRepository: AccountsCRUDRepository, private val mainAccountTokensMigration: MainAccountTokensMigration, private val cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher, + private val singleAccountListFetcher: SingleAccountListFetcher, ) { /** @@ -38,6 +42,8 @@ class RecoverCryptoPortfolioUseCase( * @param accountId the unique identifier of the account to recover */ suspend operator fun invoke(accountId: AccountId): Either = either { + fetchAccountList(userWalletId = accountId.userWalletId) + val accountList = getAccountList(userWalletId = accountId.userWalletId) ensure(accountList.canAddMoreAccounts) { @@ -62,6 +68,12 @@ class RecoverCryptoPortfolioUseCase( recoveredAccount } + private suspend fun Raise.fetchAccountList(userWalletId: UserWalletId) { + singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId)).onLeft { + raise(Error.DataOperationFailed(cause = it)) + } + } + private suspend fun Raise.getAccountList(userWalletId: UserWalletId): AccountList { return catch( block = { crudRepository.getAccountListSync(userWalletId = userWalletId) }, diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt index f492131ba6..d9e1e38b0d 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt @@ -1,14 +1,12 @@ package com.tangem.domain.account.status.utils import arrow.core.Either -import arrow.core.raise.either -import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId 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.wallet.FetchingSource import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex @@ -19,20 +17,18 @@ import timber.log.Timber * Utility class responsible for fetching and refreshing the balances of various crypto currencies * associated with a user's wallet. * - * @property accountsCRUDRepository Repository for managing account data. * @property multiNetworkStatusFetcher Fetcher for updating network statuses. * @property multiQuoteStatusFetcher Fetcher for updating quote statuses. - * @property multiYieldBalanceFetcher Fetcher for updating yield balances. + * @property multiStakingBalanceFetcher Fetcher for updating staking balances. * @property stakingIdFactory Factory for creating staking IDs. * @property parallelUpdatingScope Coroutine scope for parallel balance updates. * [REDACTED_AUTHOR] */ class CryptoCurrencyBalanceFetcher( - private val accountsCRUDRepository: AccountsCRUDRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, private val stakingIdFactory: StakingIdFactory, private val parallelUpdatingScope: CoroutineScope, ) { @@ -56,7 +52,10 @@ class CryptoCurrencyBalanceFetcher( FetchingSource.NETWORK to refreshNetworks(userWalletId = userWalletId, currencies = currencies) }, async { - FetchingSource.STAKING to refreshYieldBalances(userWalletId = userWalletId, currencies = currencies) + FetchingSource.STAKING to refreshStakingBalances( + userWalletId = userWalletId, + currencies = currencies, + ) }, async { FetchingSource.QUOTE to refreshQuotes(currencies = currencies) }, ) @@ -80,25 +79,16 @@ class CryptoCurrencyBalanceFetcher( private suspend fun refreshNetworks( userWalletId: UserWalletId, currencies: List, - ): Either = either { - val either = multiNetworkStatusFetcher( + ): Either { + return multiNetworkStatusFetcher( params = MultiNetworkStatusFetcher.Params( userWalletId = userWalletId, networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network), ), ) - - arrow.core.raise.catch( - block = { accountsCRUDRepository.syncTokens(userWalletId) }, - catch = { - Timber.e(it, "Failed to sync tokens for wallet: $userWalletId") - }, - ) - - return either } - private suspend fun refreshYieldBalances( + private suspend fun refreshStakingBalances( userWalletId: UserWalletId, currencies: List, ): Either { @@ -106,8 +96,8 @@ class CryptoCurrencyBalanceFetcher( stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() } - return multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), + return multiStakingBalanceFetcher( + params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), ) } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactory.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactory.kt index cd8bb66bb6..525875e98d 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactory.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactory.kt @@ -7,7 +7,7 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.network.getAddress import com.tangem.domain.models.quote.QuoteStatus -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency @@ -16,8 +16,8 @@ import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.single.SingleYieldBalanceProducer -import com.tangem.domain.staking.single.SingleYieldBalanceSupplier +import com.tangem.domain.staking.single.SingleStakingBalanceProducer +import com.tangem.domain.staking.single.SingleStakingBalanceSupplier import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* @@ -28,7 +28,7 @@ import javax.inject.Inject * * @property singleNetworkStatusSupplier Supplier for obtaining network status. * @property singleQuoteStatusSupplier Supplier for obtaining quote status. - * @property singleYieldBalanceSupplier Supplier for obtaining yield balance. + * @property singleStakingBalanceSupplier Supplier for obtaining staking balance. * @property stakingIdFactory Factory for creating staking IDs. * [REDACTED_AUTHOR] @@ -36,7 +36,7 @@ import javax.inject.Inject internal class CryptoCurrencyStatusesFlowFactory @Inject constructor( private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, - private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, + private val singleStakingBalanceSupplier: SingleStakingBalanceSupplier, private val stakingIdFactory: StakingIdFactory, ) { @@ -53,7 +53,7 @@ internal class CryptoCurrencyStatusesFlowFactory @Inject constructor( currency = currency, maybeNetworkStatus = statusSources.networkStatus.toOption(), maybeQuoteStatus = statusSources.quoteStatus.toOption(), - maybeYieldBalance = statusSources.yieldBalance.toOption(), + maybeStakingBalance = statusSources.stakingBalance.toOption(), ) } .onEmpty { @@ -71,10 +71,10 @@ internal class CryptoCurrencyStatusesFlowFactory @Inject constructor( ): Flow { val networkStatusFlow = getNetworkStatusFlow(userWalletId = userWallet.walletId, network = currency.network) - val yieldBalanceFlow = if (userWallet.isMultiCurrency) { + val stakingBalanceFlow = if (userWallet.isMultiCurrency) { networkStatusFlow.flatMapLatest { networkStatus -> if (networkStatus != null) { - getYieldBalanceFlow( + getStakingBalanceFlow( userWalletId = userWallet.walletId, currencyId = currency.id, networkStatus = networkStatus, @@ -89,26 +89,26 @@ internal class CryptoCurrencyStatusesFlowFactory @Inject constructor( val quoteStatusFlow = currency.id.rawCurrencyId?.let(::getQuoteStatusFlow) - return combine(networkStatusFlow, yieldBalanceFlow, quoteStatusFlow) + return combine(networkStatusFlow, stakingBalanceFlow, quoteStatusFlow) .distinctUntilChanged() } private fun combine( networkStatusFlow: Flow, - yieldBalanceFlow: Flow?, + stakingBalanceFlow: Flow?, quoteStatusFlow: Flow?, ): Flow { return when { - yieldBalanceFlow != null && quoteStatusFlow != null -> { + stakingBalanceFlow != null && quoteStatusFlow != null -> { combine( flow = networkStatusFlow, - flow2 = yieldBalanceFlow, + flow2 = stakingBalanceFlow, flow3 = quoteStatusFlow, transform = ::CryptoCurrencyStatusSources, ) } - yieldBalanceFlow != null -> { - combine(flow = networkStatusFlow, flow2 = yieldBalanceFlow, transform = ::CryptoCurrencyStatusSources) + stakingBalanceFlow != null -> { + combine(flow = networkStatusFlow, flow2 = stakingBalanceFlow, transform = ::CryptoCurrencyStatusSources) } quoteStatusFlow != null -> { combine(flow = networkStatusFlow, flow2 = quoteStatusFlow) { networkStatus, quoteStatus -> @@ -136,11 +136,11 @@ internal class CryptoCurrencyStatusesFlowFactory @Inject constructor( .distinctUntilChanged() } - private fun getYieldBalanceFlow( + private fun getStakingBalanceFlow( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, networkStatus: NetworkStatus, - ): Flow { + ): Flow { val stakingId = stakingIdFactory.create( currencyId = currencyId, defaultAddress = networkStatus.getAddress(), @@ -148,8 +148,8 @@ internal class CryptoCurrencyStatusesFlowFactory @Inject constructor( .getOrNull() return if (stakingId != null) { - singleYieldBalanceSupplier( - params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId), + singleStakingBalanceSupplier( + params = SingleStakingBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId), ) .distinctUntilChanged() } else { @@ -159,7 +159,7 @@ internal class CryptoCurrencyStatusesFlowFactory @Inject constructor( private data class CryptoCurrencyStatusSources( val networkStatus: NetworkStatus? = null, - val yieldBalance: YieldBalance? = null, + val stakingBalance: StakingBalance? = null, val quoteStatus: QuoteStatus? = null, ) } \ No newline at end of file diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt index 15c437d7aa..0b19a4cd9f 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt @@ -90,6 +90,7 @@ class DefaultSingleAccountStatusListProducerTest { ), ), totalAccounts = 1, + totalArchivedAccounts = accountList.totalArchivedAccounts, totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL), sortType = accountList.sortType, groupType = accountList.groupType, @@ -132,6 +133,7 @@ class DefaultSingleAccountStatusListProducerTest { ), ), totalAccounts = 1, + totalArchivedAccounts = accountList.totalArchivedAccounts, totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL), sortType = accountList.sortType, groupType = accountList.groupType, @@ -153,6 +155,7 @@ class DefaultSingleAccountStatusListProducerTest { ), ), totalAccounts = 1, + totalArchivedAccounts = accountList.totalArchivedAccounts, totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL), sortType = updatedAccountList.sortType, groupType = updatedAccountList.groupType, @@ -192,6 +195,7 @@ class DefaultSingleAccountStatusListProducerTest { ), ), totalAccounts = 1, + totalArchivedAccounts = accountList.totalArchivedAccounts, totalFiatBalance = TotalFiatBalance.Loaded(amount = BigDecimal.ZERO, source = StatusSource.ACTUAL), sortType = accountList.sortType, groupType = accountList.groupType, @@ -273,6 +277,7 @@ class DefaultSingleAccountStatusListProducerTest { ), ), totalAccounts = 1, + totalArchivedAccounts = accountList.totalArchivedAccounts, totalFiatBalance = TotalFiatBalance.Loading, sortType = accountList.sortType, groupType = accountList.groupType, diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt index 48026d148b..c838e40466 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt @@ -348,6 +348,7 @@ internal class ApplyTokenListSortingUseCaseTest { customAccount, // unchanged due to error ), totalAccounts = accountList.totalAccounts, + totalArchivedAccounts = accountList.totalArchivedAccounts, sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, ) diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCaseTest.kt index 109fe9d65d..d3257d26ed 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCaseTest.kt @@ -295,6 +295,7 @@ class ArchiveCryptoPortfolioUseCaseTest { ) }, totalAccounts = totalAccounts, + totalArchivedAccounts = totalArchivedAccounts, totalFiatBalance = TotalFiatBalance.Loading, sortType = sortType, groupType = groupType, diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCaseTest.kt index f6fe05e41d..8bf3eb7e56 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCaseTest.kt @@ -5,6 +5,7 @@ import arrow.core.left import arrow.core.right import arrow.core.toOption import com.google.common.truth.Truth +import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.account.repository.AccountsCRUDRepository @@ -27,17 +28,19 @@ import kotlin.random.Random class RecoverCryptoPortfolioUseCaseTest { private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val singleAccountListFetcher: SingleAccountListFetcher = mockk() private val mainAccountTokensMigration: MainAccountTokensMigration = mockk() private val cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher = mockk(relaxUnitFun = true) private val useCase = RecoverCryptoPortfolioUseCase( crudRepository = crudRepository, mainAccountTokensMigration = mainAccountTokensMigration, cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher, + singleAccountListFetcher = singleAccountListFetcher, ) @BeforeEach fun resetMocks() { - clearMocks(crudRepository, mainAccountTokensMigration, cryptoCurrencyBalanceFetcher) + clearMocks(crudRepository, mainAccountTokensMigration, cryptoCurrencyBalanceFetcher, singleAccountListFetcher) } @Test @@ -56,6 +59,7 @@ class RecoverCryptoPortfolioUseCaseTest { val updatedAccountList = (accountList + account).getOrNull()!! + coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns Unit.right() coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns archivedAccount.toOption() coEvery { mainAccountTokensMigration.migrate(userWalletId, account.derivationIndex) } returns Unit.right() @@ -69,6 +73,7 @@ class RecoverCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifySequence { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) crudRepository.getAccountListSync(userWalletId) crudRepository.getArchivedAccountSync(account.accountId) crudRepository.saveAccounts(updatedAccountList) @@ -85,6 +90,7 @@ class RecoverCryptoPortfolioUseCaseTest { derivationIndex = DerivationIndex.Companion.Main, ) + coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns Unit.right() coEvery { crudRepository.getAccountListSync(userWalletId) } returns None // Act @@ -95,7 +101,8 @@ class RecoverCryptoPortfolioUseCaseTest { Truth.assertThat(actual.cause).isInstanceOf(expected::class.java) Truth.assertThat(actual.cause).hasMessageThat().isEqualTo(expected.message) - coVerifySequence { crudRepository.getAccountListSync(userWalletId) } + coVerifySequence { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) + crudRepository.getAccountListSync(userWalletId) } coVerify(inverse = true) { crudRepository.getArchivedAccountSync(any()) crudRepository.saveAccounts(any()) @@ -111,6 +118,7 @@ class RecoverCryptoPortfolioUseCaseTest { ) val exception = IllegalStateException("Test error") + coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns Unit.right() coEvery { crudRepository.getAccountListSync(userWalletId) } throws exception // Act @@ -120,7 +128,8 @@ class RecoverCryptoPortfolioUseCaseTest { val expected = DataOperationFailed(exception).left() Truth.assertThat(actual).isEqualTo(expected) - coVerifySequence { crudRepository.getAccountListSync(userWalletId) } + coVerifySequence { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) + crudRepository.getAccountListSync(userWalletId) } coVerify(inverse = true) { crudRepository.getArchivedAccountSync(any()) crudRepository.saveAccounts(any()) @@ -134,6 +143,7 @@ class RecoverCryptoPortfolioUseCaseTest { val accountList = AccountList.Companion.empty(userWalletId) val exception = IllegalStateException("Test error") + coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns Unit.right() coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() coEvery { crudRepository.getArchivedAccountSync(account.accountId) } throws exception @@ -145,6 +155,7 @@ class RecoverCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifySequence { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) crudRepository.getAccountListSync(userWalletId) crudRepository.getArchivedAccountSync(account.accountId) } @@ -157,6 +168,7 @@ class RecoverCryptoPortfolioUseCaseTest { val account = createAccount(userWalletId) val accountList = AccountList.Companion.empty(userWalletId) + coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns Unit.right() coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns None @@ -169,6 +181,7 @@ class RecoverCryptoPortfolioUseCaseTest { Truth.assertThat(actual.cause).hasMessageThat().isEqualTo(expected.message) coVerifySequence { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) crudRepository.getAccountListSync(userWalletId) crudRepository.getArchivedAccountSync(account.accountId) } @@ -192,6 +205,7 @@ class RecoverCryptoPortfolioUseCaseTest { val updatedAccountList = (accountList + account).getOrNull()!! val exception = IllegalStateException("Save failed") + coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns Unit.right() coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns archivedAccount.toOption() coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception @@ -204,12 +218,41 @@ class RecoverCryptoPortfolioUseCaseTest { Truth.assertThat(actual).isEqualTo(expected) coVerifySequence { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) crudRepository.getAccountListSync(userWalletId) crudRepository.getArchivedAccountSync(account.accountId) crudRepository.saveAccounts(updatedAccountList) } } + @Test + fun `invoke should return error if fetch is failed`() = runTest { + // Arrange + val accountId = AccountId.Companion.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex.Companion.Main, + ) + val exception = IllegalStateException("Test error") + + coEvery { singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) } returns exception.left() + + // Act + val actual = useCase(accountId) + + // Assert + val expected = DataOperationFailed(exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifySequence { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) + } + coVerify(inverse = true) { + crudRepository.getAccountListSync(any()) + crudRepository.getArchivedAccountSync(any()) + crudRepository.saveAccounts(any()) + } + } + private fun createAccount( userWalletId: UserWalletId, name: String = "Test Account", diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2Test.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2Test.kt index 016e88d152..9dadb85e4f 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2Test.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2Test.kt @@ -42,6 +42,7 @@ class ToggleTokenListGroupingUseCaseV2Test { userWalletId = userWalletId, accountStatuses = emptyList(), totalAccounts = 0, + totalArchivedAccounts = 0, totalFiatBalance = TotalFiatBalance.Failed, sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, @@ -205,6 +206,7 @@ class ToggleTokenListGroupingUseCaseV2Test { userWalletId = userWalletId, accountStatuses = listOf(accountStatus), totalAccounts = 1, + totalArchivedAccounts = 0, totalFiatBalance = tokenList.totalFiatBalance, sortType = tokenList.sortedBy, groupType = groupType, diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2Test.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2Test.kt index d5806c8bb1..344dc8641e 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2Test.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2Test.kt @@ -42,6 +42,7 @@ class ToggleTokenListSortingUseCaseV2Test { userWalletId = userWalletId, accountStatuses = emptyList(), totalAccounts = 0, + totalArchivedAccounts = 0, totalFiatBalance = TotalFiatBalance.Failed, sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, @@ -145,6 +146,7 @@ class ToggleTokenListSortingUseCaseV2Test { userWalletId = userWalletId, accountStatuses = listOf(accountStatus), totalAccounts = 1, + totalArchivedAccounts = 0, totalFiatBalance = tokenList.totalFiatBalance, sortType = tokenList.sortedBy, groupType = TokensGroupType.NONE, diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactoryTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactoryTest.kt index a5df234f9a..65f8d8e607 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactoryTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactoryTest.kt @@ -11,7 +11,7 @@ import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency @@ -20,8 +20,8 @@ import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.single.SingleYieldBalanceProducer -import com.tangem.domain.staking.single.SingleYieldBalanceSupplier +import com.tangem.domain.staking.single.SingleStakingBalanceProducer +import com.tangem.domain.staking.single.SingleStakingBalanceSupplier import com.tangem.test.core.getEmittedValues import io.mockk.* import kotlinx.coroutines.flow.emptyFlow @@ -41,13 +41,13 @@ class CryptoCurrencyStatusesFlowFactoryTest { private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier = mockk() private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier = mockk() - private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier = mockk() + private val singleStakingBalanceSupplier: SingleStakingBalanceSupplier = mockk() private val stakingIdFactory: StakingIdFactory = mockk() private val factory = CryptoCurrencyStatusesFlowFactory( singleNetworkStatusSupplier = singleNetworkStatusSupplier, singleQuoteStatusSupplier = singleQuoteStatusSupplier, - singleYieldBalanceSupplier = singleYieldBalanceSupplier, + singleStakingBalanceSupplier = singleStakingBalanceSupplier, stakingIdFactory = stakingIdFactory, ) @@ -63,7 +63,7 @@ class CryptoCurrencyStatusesFlowFactoryTest { clearMocks( singleNetworkStatusSupplier, singleQuoteStatusSupplier, - singleYieldBalanceSupplier, + singleStakingBalanceSupplier, stakingIdFactory, ) } @@ -96,13 +96,13 @@ class CryptoCurrencyStatusesFlowFactoryTest { stakingIdFactory.create(currencyId = currency.id, defaultAddress = networkAddress.defaultAddress.value) } returns stakingId.right() - val yieldBalance = YieldBalance.Empty(stakingId = stakingId, source = StatusSource.ACTUAL) - val yieldBalanceFlow = flowOf(yieldBalance) + val stakingBalance = StakingBalance.Empty(stakingId = stakingId, source = StatusSource.ACTUAL) + val stakingBalanceFlow = flowOf(stakingBalance) every { - singleYieldBalanceSupplier( - params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId), + singleStakingBalanceSupplier( + params = SingleStakingBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId), ) - } returns yieldBalanceFlow + } returns stakingBalanceFlow // Act val actual = factory.create(userWallet = userWallet, currency = currency).let(::getEmittedValues) @@ -121,7 +121,7 @@ class CryptoCurrencyStatusesFlowFactoryTest { coVerify(ordering = Ordering.SEQUENCE) { singleNetworkStatusSupplier(params = SingleNetworkStatusProducer.Params(userWalletId, currency.network)) stakingIdFactory.create(currencyId = currency.id, defaultAddress = networkAddress.defaultAddress.value) - singleYieldBalanceSupplier(params = SingleYieldBalanceProducer.Params(userWalletId, stakingId)) + singleStakingBalanceSupplier(params = SingleStakingBalanceProducer.Params(userWalletId, stakingId)) } } diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/TokenListExt.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/TokenListExt.kt index 0745e8a650..da013e557b 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/TokenListExt.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/TokenListExt.kt @@ -44,7 +44,7 @@ internal fun createStatus(currency: CryptoCurrency, fiatAmount: BigDecimal): Cry fiatRate = BigDecimal.ONE, fiatAmount = fiatAmount, priceChange = BigDecimal.ZERO, - yieldBalance = null, + stakingBalance = null, hasCurrentNetworkTransactions = false, yieldSupplyStatus = null, pendingTransactions = emptySet(), diff --git a/domain/balance-hiding/detekt-baseline-main.xml b/domain/balance-hiding/detekt-baseline-main.xml new file mode 100644 index 0000000000..6917fe3471 --- /dev/null +++ b/domain/balance-hiding/detekt-baseline-main.xml @@ -0,0 +1,9 @@ + + + + + MultilineLambdaItParameter:ListenToFlipsUseCase.kt$ListenToFlipsUseCase${ send(HideBalancesError.DataError(it).left()) return@collectLatest } + NoNameShadowing:ListenToFlipsUseCase.kt$ListenToFlipsUseCase${ send(HideBalancesError.DataError(it).left()) return@collectLatest } + NoNameShadowing:ListenToFlipsUseCase.kt$ListenToFlipsUseCase${ send(HideBalancesError.DataError(it).left()) } + + diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt index 15916c16cf..b7151afd3d 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt @@ -1,14 +1,26 @@ package com.tangem.domain.card.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam sealed class IntroductionProcess( event: String, params: Map = emptyMap(), ) : AnalyticsEvent("Introduction Process", event, params) { - object ScreenOpened : IntroductionProcess("Introduction Process Screen Opened") - object ButtonTokensList : IntroductionProcess("Button - Tokens List") - object ButtonBuyCards : IntroductionProcess("Button - Buy Cards") - object ButtonScanCard : IntroductionProcess("Button - Scan Card") + class ScreenOpened : IntroductionProcess("Introduction Process Screen Opened") + class ButtonTokensList : IntroductionProcess("Button - Tokens List") + class ButtonBuyCards : IntroductionProcess("Button - Buy Cards") + class ButtonScanCardLegacy : IntroductionProcess("Button - Scan Card") + + class CreateWalletIntroScreenOpened : IntroductionProcess("Create Wallet Intro Screen Opened") + + class ButtonScanCard( + val source: AnalyticsParam.ScreensSources, + ) : IntroductionProcess( + event = "Button - Scan Card", + params = mapOf( + AnalyticsParam.Key.SOURCE to source.value, + ), + ) } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt index 11536725f8..37ae7f2702 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt @@ -7,5 +7,5 @@ sealed class Shop( params: Map = emptyMap(), ) : AnalyticsEvent("Shop", event, params) { - object ScreenOpened : Shop("Shop Screen Opened") + class ScreenOpened : Shop("Shop Screen Opened") } \ No newline at end of file diff --git a/domain/core/detekt-baseline-main.xml b/domain/core/detekt-baseline-main.xml new file mode 100644 index 0000000000..c27890d54a --- /dev/null +++ b/domain/core/detekt-baseline-main.xml @@ -0,0 +1,11 @@ + + + + + IgnoredReturnValue:FlowCachingSupplier.kt$FlowCachingSupplier$put(key = key, value = flow) + IgnoredReturnValue:FlowCachingSupplier.kt$FlowCachingSupplier$remove(key) + MultilineLambdaItParameter:FlowCachingSupplier.kt$FlowCachingSupplier${ it.toMutableMap().apply { put(key = key, value = flow) } } + MultilineLambdaItParameter:FlowCachingSupplier.kt$FlowCachingSupplier${ it.toMutableMap().apply { remove(key) } } + ObjectExtendsThrowable:DataError.kt$DataError.NetworkError$NoInternetConnection : NetworkError + + diff --git a/domain/demo/models/src/main/kotlin/com/tangem/domain/demo/models/DemoConfigCardIds.kt b/domain/demo/models/src/main/kotlin/com/tangem/domain/demo/models/DemoConfigCardIds.kt new file mode 100644 index 0000000000..c9a8d5dc7f --- /dev/null +++ b/domain/demo/models/src/main/kotlin/com/tangem/domain/demo/models/DemoConfigCardIds.kt @@ -0,0 +1,397 @@ +package com.tangem.domain.demo.models + +@Suppress("LargeClass") +internal object DemoConfigCardIds { + + val releaseDemoCardIds = mutableListOf( + // === Not from the Google Sheet table === + "AC01000000041225", + "AC01000000041472", + "AB01000000046498", + "AB01000000049608", + "AB01000000049574", + "AB01000000046704", + "AB02000000051000", + "AB02000000050911", + + // === Mvideo === + // Wallet + "AC01000000045754", + "AC01000000041662", + "AC01000000041647", + "AC01000000041209", + "AC01000000042462", + "AC01000000041100", + "AC01000000041621", + "AC01000000045960", + "AC01000000041092", + "AC01000000041217", + "AC01000000013489", + "AC01000000028610", + "AC01000000028701", + "AC01000000028578", + "AC01000000027281", + "AC01000000027216", + "AC01000000028594", + "AC01000000028602", + "AC01000000028636", + "AC01000000013968", + "AC01000000027208", + "AC01000000013471", + "AC01000000028586", + "AC01000000013703", + "AC01000000028628", + "AC01000000028693", + "AC01000000028685", + "AC01000000013950", + "AC01000000013828", + "AC01000000013497", + "AC01000000013836", + "AC01000000013505", + "AC03000000046693", + "AC03000000046685", + "AC03000000046677", + "AC03000000046669", + "AC03000000046651", + "AC03000000046644", + "AC03000000046636", + "AC03000000046628", + "AC03000000046610", + "AC03000000046602", + "AC03000000046594", + "AC03000000046586", + "AC03000000046578", + "AC03000000046560", + "AC03000000046552", + "AC03000000046545", + "AC03000000046537", + "AC03000000046529", + "AC03000000046511", + "AC03000000046800", + "AC03000000046792", + "AC03000000046784", + "AC03000000046776", + "AC03000000046768", + "AC03000000046750", + "AC03000000046743", + "AC03000000046735", + "AC03000000046727", + "AC03000000046446", + "AC03000000046438", + "AC03000000046412", + "AC03000000046388", + "AC03000000046370", + "AC03000000046354", + "AC03000000046347", + "AC03000000046339", + "AC03000000046321", + "AC03000000046172", + "AC03000000046396", + "AC03000000046404", + "AC03000000046701", + "AC03000000046420", + "AC03000000046719", + "AC03000000046503", + "AC03000000046495", + "AC03000000046487", + "AC03000000046362", + "AC03000000046479", + "AC03000000046461", + "AC03000000046453", + + // Note BTC + "AB01000000059608", + "AB01000000046647", + "AB01000000046571", + "AB01000000046746", + "AB01000000059574", + "AB01000000046753", + "AB01000000046605", + "AB01000000046761", + "AB01000000046720", + "AB01000000046530", + "AB01000000016475", + "AB01000000016483", + "AB01000000016491", + "AB01000000020709", + "AB01000000020717", + "AB01000000015550", + "AB01000000015394", + "AB01000000016079", + "AB01000000016087", + "AB01000000016095", + "AB01000000020915", + "AB01000000017184", + "AB01000000020907", + "AB01000000017192", + "AB01000000016210", + "AB01000000016111", + "AB01000000016103", + "AB01000000015766", + "AB01000000015774", + "AB01000000015782", + "AB01000000022598", + "AB01000000022580", + "AB01000000005688", + "AB07000000005696", + "AB07000000005902", + "AB07000000005910", + "AB07000000005928", + "AB07000000005936", + "AB07000000005944", + "AB07000000005993", + "AB07000000005985", + "AB07000000005977", + "AB07000000005969", + "AB07000000005951", + "AB07000000005605", + "AB07000000005803", + "AB07000000005811", + "AB07000000005829", + "AB07000000005837", + "AB07000000005845", + "AB07000000005852", + "AB07000000005860", + "AB07000000005878", + "AB07000000005886", + "AB07000000005894", + "AB07000000005704", + "AB07000000005712", + "AB07000000005720", + "AB07000000005738", + "AB07000000005746", + "AB07000000005514", + "AB07000000005522", + "AB07000000005563", + "AB07000000005571", + "AB07000000005589", + "AB07000000005597", + "AB07000000005613", + "AB07000000005621", + "AB07000000005639", + "AB07000000005647", + "AB07000000005654", + "AB07000000005662", + "AB07000000005670", + "AB07000000005530", + "AB07000000005548", + "AB07000000005555", + "AB07000000005753", + "AB07000000005761", + "AB07000000005779", + "AB07000000005787", + "AB07000000005795", + "AB07000000005506", + + // Note ETH + "AB02000000051083", + "AB02000000051059", + "AB02000000051158", + "AB02000000050986", + "AB02000000051026", + "AB02000000050960", + "AB02000000051042", + "AB02000000051091", + "AB02000000051034", + "AB02000000051133", + "AB02000000019924", + "AB02000000019932", + "AB02000000022092", + "AB02000000022282", + "AB02000000023983", + "AB02000000023439", + "AB02000000020328", + "AB02000000020310", + "AB02000000021565", + "AB02000000022357", + "AB02000000023355", + "AB02000000022324", + "AB02000000022100", + "AB02000000019999", + "AB02000000020013", + "AB02000000020005", + "AB02000000020021", + "AB02000000020039", + "AB02000000020278", + "AB02000000020252", + "AB02000000018652", + "AB02000000018561", + "AB08000000009481", + "AB08000000009473", + "AB08000000009705", + "AB08000000009897", + "AB08000000009689", + "AB08000000009671", + "AB08000000009465", + "AB08000000009457", + "AB08000000009440", + "AB08000000009432", + "AB08000000009424", + "AB08000000009416", + "AB08000000009408", + "AB08000000009390", + "AB08000000009374", + "AB08000000009382", + "AB08000000009267", + "AB08000000009275", + "AB08000000009283", + "AB08000000009291", + "AB08000000009309", + "AB08000000009317", + "AB08000000009325", + "AB08000000009333", + "AB08000000009341", + "AB08000000009358", + "AB08000000009366", + "AB08000000009077", + "AB08000000009143", + "AB08000000009168", + "AB08000000009184", + "AB08000000009192", + "AB08000000009200", + "AB08000000009226", + "AB08000000009218", + "AB08000000009234", + "AB08000000009242", + "AB08000000008574", + "AB08000000009069", + "AB08000000008525", + "AB08000000009051", + "AB08000000009135", + "AB08000000009150", + "AB08000000009176", + "AB08000000009085", + "AB08000000009093", + "AB08000000009101", + "AB08000000009119", + "AB08000000009127", + "AB08000000009259", + + // === Technopark === + // Wallet + "AC01000000044120", + "AC01000000044997", + "AC01000000044989", + "AC01000000043494", + "AC01000000043486", + "AC01000000044187", + "AC01000000043148", + "AC01000000044013", + "AC01000000043973", + "AC01000000044815", + "AC01000000044807", + "AC01000000043809", + "AC01000000043833", + "AC01000000043460", + "AC01000000043064", + "AC01000000044138", + "AC01000000044500", + "AC01000000044492", + "AC01000000044260", + "AC01000000044278", + + // Note BTC + "AB01000000049864", + "AB01000000053239", + "AB01000000053056", + "AB01000000054237", + "AB01000000054245", + "AB01000000054211", + "AB01000000054229", + "AB01000000053189", + "AB01000000054195", + "AB01000000050797", + "AB01000000053833", + "AB01000000052124", + "AB01000000051605", + "AB01000000052223", + "AB01000000052207", + "AB01000000052199", + "AB01000000047785", + "AB01000000047850", + "AB01000000047868", + "AB01000000048288", + + // Note ETH + "AB02000000049715", + "AB02000000049848", + "AB02000000049814", + "AB02000000049863", + "AB02000000049871", + "AB02000000049855", + "AB02000000049285", + "AB02000000049277", + "AB02000000049558", + "AB02000000049889", + "AB02000000049988", + "AB02000000049707", + "AB02000000049699", + "AB02000000049897", + "AB02000000049905", + "AB02000000049913", + "AB02000000049251", + "AB02000000049533", + "AB02000000049541", + "AB02000000049830", + // === more cids === + "AC03000000091418", + "AC03000000091400", + "AC03000000099007", + "AC03000000098991", + "AC03000000098942", + "AC03000000091715", + "AC03000000091301", + "AC03000000091343", + "AB01000000055705", + "AB01000000052918", + "AB01000000047710", + "AB01000000052306", + "AB01000000047645", + "AB01000000048957", + "AB01000000052900", + "AB01000000050391", + "AB01000000047363", + "AB02000000053998", + "AB02000000019809", + "AB02000000020872", + "AB02000000022027", + "AB02000000058955", + "AB02000000053253", + "AB02000000048063", + "AB02000000023736", + "AB02000000058187", + "AB02000000000007", + "AC03000000076229", + "AF04000000000118", + + // Wallet 2 + "AF04000000012006", + "AF04000000012014", + "AF04000000012022", + "AF04000000012030", + "AF15000000257889", + "AF15000000257897", + "AF15000000637809", + "AF15000000640282", + "AF15000001187424", + "AF15000001187408", + "AF15000001187416", + "AF15000001195781", + "AF15000001195773", + "AF15000001195799", + // Wallet 2 QA + "AF12345678912346", + "AF12345678912361", + "AF10100000000076", + "AF10100000000084", + ) + + val testDemoCardIds = listOf( + "FB20000000000186", // Note ETH + "FB10000000000196", // Note BTC + "FB30000000000176", // Wallet + "FB04000000000152", // Wallet 2 + ) + + val debugTestDemoCardIds = emptyList() +} \ No newline at end of file diff --git a/domain/express/models/detekt-baseline-main.xml b/domain/express/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..0c9aed8c52 --- /dev/null +++ b/domain/express/models/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + ObjectExtendsThrowable:ExpressError.kt$ExpressError$UnknownError : ExpressError + + diff --git a/domain/feedback/models/detekt-baseline-main.xml b/domain/feedback/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..4d318997ac --- /dev/null +++ b/domain/feedback/models/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + BooleanPropertyNaming:WalletMetaInfo.kt$WalletMetaInfo$val hotWalletIsBackedUp: Boolean? = null + + diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/IsAccessCodeSimpleUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/IsAccessCodeSimpleUseCase.kt new file mode 100644 index 0000000000..04751ad562 --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/IsAccessCodeSimpleUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.hotwallet + +class IsAccessCodeSimpleUseCase { + operator fun invoke(accessCode: String): Boolean { + return isSequential(accessCode) || isRepeatedCharacters(accessCode) + } + + fun isSequential(code: String): Boolean = code.length > 1 && + (code.zipWithNext().all { it.second == it.first + 1 } || + code.zipWithNext().all { it.second == it.first - 1 }) + + fun isRepeatedCharacters(code: String): Boolean = code.isNotEmpty() && code.all { it == code.first() } +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/IsHotWalletCreationSupported.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/IsHotWalletCreationSupported.kt new file mode 100644 index 0000000000..f3e4a3c05f --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/IsHotWalletCreationSupported.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.hotwallet + +import com.tangem.domain.hotwallet.repository.HotWalletRepository + +class IsHotWalletCreationSupported(private val hotWalletRepository: HotWalletRepository) { + operator fun invoke(): Boolean = hotWalletRepository.isWalletCreationSupported() + + fun getLeastVersionName(): String = hotWalletRepository.getLeastSupportedAndroidVersionName() +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt index 5bc636f2dd..e228cd4419 100644 --- a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt @@ -5,6 +5,10 @@ import kotlinx.coroutines.flow.Flow interface HotWalletRepository { + fun isWalletCreationSupported(): Boolean + + fun getLeastSupportedAndroidVersionName(): String + fun accessCodeSkipped(userWalletId: UserWalletId): Flow suspend fun setAccessCodeSkipped(userWalletId: UserWalletId, skipped: Boolean) diff --git a/domain/legacy/detekt-baseline-debug.xml b/domain/legacy/detekt-baseline-debug.xml deleted file mode 100644 index 3d02fabec6..0000000000 --- a/domain/legacy/detekt-baseline-debug.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - BooleanPropertyNaming:LogConfig.kt$AnalyticsHandlersLogConfig$val amplitude: Boolean = BuildConfig.LOG_ENABLED - BooleanPropertyNaming:LogConfig.kt$AnalyticsHandlersLogConfig$val appsflyer: Boolean = BuildConfig.LOG_ENABLED - BooleanPropertyNaming:LogConfig.kt$AnalyticsHandlersLogConfig$val firebase: Boolean = BuildConfig.LOG_ENABLED - BooleanPropertyNaming:LogConfig.kt$LogConfig$val storeAction: Boolean = BuildConfig.LOG_ENABLED - BooleanPropertyNaming:LogConfig.kt$NetworkLogConfig$val blockchainSdkNetwork: Boolean = BuildConfig.LOG_ENABLED - - diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt index 3adab018df..1a8068cfb5 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt @@ -4,17 +4,17 @@ import com.tangem.domain.features.BuildConfig object LogConfig { const val imageLoader: Boolean = false - val storeAction: Boolean = BuildConfig.LOG_ENABLED + val shouldStoreAction: Boolean = BuildConfig.LOG_ENABLED val network: NetworkLogConfig = NetworkLogConfig val analyticsHandlers: AnalyticsHandlersLogConfig = AnalyticsHandlersLogConfig } object NetworkLogConfig { - val blockchainSdkNetwork: Boolean = BuildConfig.LOG_ENABLED + val isBlockchainSdkNetworkLogEnabled: Boolean = BuildConfig.LOG_ENABLED } object AnalyticsHandlersLogConfig { - val firebase: Boolean = BuildConfig.LOG_ENABLED - val amplitude: Boolean = BuildConfig.LOG_ENABLED - val appsflyer: Boolean = BuildConfig.LOG_ENABLED + val isFirebaseLogEnabled: Boolean = BuildConfig.LOG_ENABLED + val isAmplitudeLogEnabled: Boolean = BuildConfig.LOG_ENABLED + val isAppsflyerLogEnabled: Boolean = BuildConfig.LOG_ENABLED } \ No newline at end of file diff --git a/domain/manage-tokens/build.gradle.kts b/domain/manage-tokens/build.gradle.kts index e5fffdace4..484176877c 100644 --- a/domain/manage-tokens/build.gradle.kts +++ b/domain/manage-tokens/build.gradle.kts @@ -24,6 +24,8 @@ dependencies { implementation(projects.domain.wallets) implementation(projects.domain.legacy) + implementation(tangemDeps.blockchain) + /* Core */ api(projects.core.pagination) testImplementation(projects.core.pagination) diff --git a/domain/manage-tokens/detekt-baseline-debug.xml b/domain/manage-tokens/detekt-baseline-debug.xml deleted file mode 100644 index 5c9a9d5ec2..0000000000 --- a/domain/manage-tokens/detekt-baseline-debug.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - NamedArguments:CheckIsCurrencyNotAddedUseCase.kt$CheckIsCurrencyNotAddedUseCase$isCurrencyNotAdded(userWalletId, networkId, derivationPath, contractAddress) - NamedArguments:CreateCryptoCurrencyUseCase.kt$CreateCryptoCurrencyUseCase$createCustomToken(userWalletId, networkId, derivationPath, formValues) - NamedArguments:ValidateTokenFormUseCase.kt$ValidateTokenFormUseCase$zipOrAccumulate( { ensureIsContractAddressValid(formValues.contractAddress, networkId) }, { ensureIsDecimalsValid(formValues.decimals) }, { ensure(formValues.name.isNotBlank()) { CustomTokenFormValidationException.EmptyName } }, { ensure(formValues.symbol.isNotBlank()) { CustomTokenFormValidationException.EmptySymbol } }, ) { contractAddress, decimals, _, _ -> AddCustomTokenForm.Validated.All( contractAddress = contractAddress, symbol = formValues.symbol, name = formValues.name, decimals = decimals, ) } - - diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckIsCurrencyNotAddedUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckIsCurrencyNotAddedUseCase.kt index e6bd36e0a3..51b7543912 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckIsCurrencyNotAddedUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CheckIsCurrencyNotAddedUseCase.kt @@ -15,6 +15,11 @@ class CheckIsCurrencyNotAddedUseCase( derivationPath: Network.DerivationPath, contractAddress: String?, ): Either = Either.catch { - repository.isCurrencyNotAdded(userWalletId, networkId, derivationPath, contractAddress) + repository.isCurrencyNotAdded( + userWalletId = userWalletId, + networkId = networkId, + derivationPath = derivationPath, + contractAddress = contractAddress, + ) } } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCryptoCurrencyUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCryptoCurrencyUseCase.kt index 1b305b4812..359c4011fb 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCryptoCurrencyUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/CreateCryptoCurrencyUseCase.kt @@ -57,9 +57,18 @@ class CreateCryptoCurrencyUseCase( formValues: AddCustomTokenForm.Validated.All?, ): Either = Either.catch { if (formValues == null) { - customTokensRepository.createCoin(userWalletId, networkId, derivationPath) + customTokensRepository.createCoin( + userWalletId = userWalletId, + networkId = networkId, + derivationPath = derivationPath, + ) } else { - customTokensRepository.createCustomToken(userWalletId, networkId, derivationPath, formValues) + customTokensRepository.createCustomToken( + userWalletId = userWalletId, + networkId = networkId, + derivationPath = derivationPath, + formValues = formValues, + ) } } } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt index 51fbc16bab..3fd2900bd7 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt @@ -10,7 +10,7 @@ import com.tangem.domain.models.wallet.UserWalletId 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 @@ -28,7 +28,7 @@ class SaveManagedTokensUseCase( private val derivationsRepository: DerivationsRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, private val stakingIdFactory: StakingIdFactory, private val parallelUpdatingScope: CoroutineScope, ) { @@ -61,6 +61,8 @@ class SaveManagedTokensUseCase( parallelUpdatingScope.launch { withContext(NonCancellable) { + syncTokens(userWalletId = userWalletId, addedCurrencies = savedCurrencies) + launch { refreshUpdatedNetworks( userWalletId = userWalletId, @@ -68,7 +70,7 @@ class SaveManagedTokensUseCase( ) } launch { - refreshUpdatedYieldBalances( + refreshUpdatedStakingBalances( userWalletId = userWalletId, addedCurrencies = savedCurrencies, ) @@ -96,6 +98,26 @@ class SaveManagedTokensUseCase( ) } + private suspend fun syncTokens(userWalletId: UserWalletId, addedCurrencies: List) { + createWalletManagers(userWalletId = userWalletId, currencies = addedCurrencies) + currenciesRepository.syncTokens(userWalletId) + } + + /** + * Creates wallet managers for the given [currencies] if they do not already exist. + * The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature. + * + * @param userWalletId The ID of the user's wallet. + * @param currencies The list of cryptocurrencies for which to create wallet managers. + */ + private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List) { + val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network) + + for (network in networks) { + walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network) + } + } + private suspend fun refreshUpdatedNetworks(userWalletId: UserWalletId, addedCurrencies: List) { multiNetworkStatusFetcher( MultiNetworkStatusFetcher.Params( @@ -103,11 +125,9 @@ class SaveManagedTokensUseCase( networks = addedCurrencies.map(CryptoCurrency::network).toSet(), ), ) - - currenciesRepository.syncTokens(userWalletId) } - private suspend fun refreshUpdatedYieldBalances( + private suspend fun refreshUpdatedStakingBalances( userWalletId: UserWalletId, addedCurrencies: List, ) { @@ -115,8 +135,8 @@ class SaveManagedTokensUseCase( stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() } - multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), + multiStakingBalanceFetcher( + params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), ) } diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt index d909ca4c6a..30ec3baf72 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt @@ -12,6 +12,7 @@ class ValidateTokenFormUseCase( private val repository: CustomTokensRepository, ) { + @Suppress("NamedArguments") suspend operator fun invoke( networkId: Network.ID, formValues: AddCustomTokenForm.Raw, diff --git a/domain/markets/build.gradle.kts b/domain/markets/build.gradle.kts index 735657a25b..c783650c9d 100644 --- a/domain/markets/build.gradle.kts +++ b/domain/markets/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { api(projects.domain.networks) api(projects.domain.staking) api(projects.domain.quotes) + api(projects.domain.walletManager) api(projects.domain.wallets) api(projects.domain.wallets.models) api(projects.domain.promo) diff --git a/domain/markets/models/detekt-baseline-main.xml b/domain/markets/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..17228539ba --- /dev/null +++ b/domain/markets/models/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + BooleanPropertyNaming:TokenMarketInfo.kt$TokenMarketInfo.Network$val exchangeable: Boolean + + diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt index c1d1b50552..08b7e83e21 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt @@ -9,8 +9,9 @@ import com.tangem.domain.models.wallet.UserWalletId 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 import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.NonCancellable @@ -31,10 +32,11 @@ import kotlinx.coroutines.withContext class SaveMarketTokensUseCase( private val derivationsRepository: DerivationsRepository, private val marketsTokenRepository: MarketsTokenRepository, + private val walletManagersFacade: WalletManagersFacade, private val currenciesRepository: CurrenciesRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, private val stakingIdFactory: StakingIdFactory, private val parallelUpdatingScope: CoroutineScope, ) { @@ -80,14 +82,36 @@ class SaveMarketTokensUseCase( parallelUpdatingScope.launch { withContext(NonCancellable) { + syncTokens(userWalletId, savedCurrencies) + launch { refreshUpdatedNetworks(userWalletId, savedCurrencies) } - launch { refreshUpdatedYieldBalances(userWalletId, savedCurrencies) } + launch { refreshUpdatedStakingBalances(userWalletId, savedCurrencies) } launch { refreshUpdatedQuotes(savedCurrencies) } } } } } + private suspend fun syncTokens(userWalletId: UserWalletId, addedCurrencies: List) { + createWalletManagers(userWalletId = userWalletId, currencies = addedCurrencies) + currenciesRepository.syncTokens(userWalletId) + } + + /** + * Creates wallet managers for the given [currencies] if they do not already exist. + * The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature. + * + * @param userWalletId The ID of the user's wallet. + * @param currencies The list of cryptocurrencies for which to create wallet managers. + */ + private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List) { + val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network) + + for (network in networks) { + walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network) + } + } + private suspend fun refreshUpdatedNetworks(userWalletId: UserWalletId, addedCurrencies: List) { multiNetworkStatusFetcher( MultiNetworkStatusFetcher.Params( @@ -95,10 +119,9 @@ class SaveMarketTokensUseCase( networks = addedCurrencies.map(CryptoCurrency::network).toSet(), ), ) - currenciesRepository.syncTokens(userWalletId) } - private suspend fun refreshUpdatedYieldBalances( + private suspend fun refreshUpdatedStakingBalances( userWalletId: UserWalletId, existingCurrencies: List, ) { @@ -106,8 +129,8 @@ class SaveMarketTokensUseCase( stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() } - multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), + multiStakingBalanceFetcher( + params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), ) } diff --git a/domain/models/detekt-baseline-main.xml b/domain/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..c26371c864 --- /dev/null +++ b/domain/models/detekt-baseline-main.xml @@ -0,0 +1,27 @@ + + + + + BooleanPropertyNaming:ShortArticle.kt$ShortArticle$val viewed: Boolean + BooleanPropertyNaming:TokenReceiveConfig.kt$TokenReceiveConfig$val showMemoDisclaimer: Boolean + BooleanPropertyNaming:UserWallet.kt$UserWallet.Hot$val backedUp: Boolean + BooleanPropertyNaming:YieldBalanceItem.kt$PendingAction.PendingActionArgs$val signatureVerification: Boolean? + BooleanPropertyNaming:YieldBalanceItem.kt$PendingAction.PendingActionArgs$val validatorAddress: Boolean? + BooleanPropertyNaming:YieldBalanceItem.kt$PendingAction.PendingActionArgs$val validatorAddresses: Boolean? + BooleanPropertyNaming:YieldBalanceItem.kt$PendingAction.PendingActionArgs.Amount$val required: Boolean + BooleanPropertyNaming:YieldBalanceItem.kt$PendingAction.PendingActionArgs.Duration$val required: Boolean + BooleanPropertyNaming:YieldBalanceItem.kt$PendingAction.PendingActionArgs.TronResource$val required: Boolean + CastNullableToNonNullableType:DerivationPathAdapterWithMigration.kt$DerivationPathAdapterWithMigration$as + MultilineLambdaItParameter:MobileWallet.kt$MobileWallet${ ExtendedPublicKey( publicKey = publicKey, chainCode = it, ) } + NoNameShadowing:Account.kt$Account.CryptoPortfolio.Companion$derivationIndex + NullableBooleanCheck:CryptoCurrency.kt$CryptoCurrency$iconUrl?.isNotBlank() ?: true + NullableToStringCall:AccountName.kt$AccountName.Error.Empty$${Empty::class.simpleName} + NullableToStringCall:AccountName.kt$AccountName.Error.ExceedsMaxLength$${ExceedsMaxLength::class.simpleName} + NullableToStringCall:DerivationIndex.kt$DerivationIndex.Error.NegativeDerivationIndex$${this::class.simpleName} + UnsafeCallOnNullableType:MobileWalletAsStringSerializer.kt$MobileWalletAsStringSerializer$moshi.adapter(MobileWallet::class.java).fromJson(decoder.decodeString())!! + UnsafeCallOnNullableType:ScanResponseAsStringSerializer.kt$ScanResponseAsStringSerializer$moshi.adapter(ScanResponse::class.java).fromJson(decoder.decodeString())!! + UseEmptyCounterpart:ScanResponse.kt$ScanResponse$mapOf() + UseOrEmpty:CardDTO.kt$CardDTO.FirmwareVersion$type.rawValue ?: "" + UseOrEmpty:UserWalletId.kt$UserWalletId$value?.toHexString() ?: "" + + diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyStatus.kt index 0663a46f9f..967a0c2b07 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyStatus.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyStatus.kt @@ -5,7 +5,7 @@ import com.tangem.domain.models.getResultStatusSource import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.serialization.SerializedBigDecimal -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.yield.supply.YieldSupplyStatus import kotlinx.serialization.Serializable @@ -56,13 +56,10 @@ data class CryptoCurrencyStatus( /** The network address */ val networkAddress: NetworkAddress? get() = null - /** Staking yield balance */ - val yieldBalance: YieldBalance? get() = null + /** Staking balance */ + val stakingBalance: StakingBalance? get() = null - /** - * !!! DO NOT CONFUSE with STAKING YIELD BALANCE - * Yield supply status - */ + /** Yield supply status */ val yieldSupplyStatus: YieldSupplyStatus? get() = null /** Sources */ @@ -73,11 +70,11 @@ data class CryptoCurrencyStatus( data class Sources( val networkSource: StatusSource = StatusSource.ACTUAL, val quoteSource: StatusSource = StatusSource.ACTUAL, - val yieldBalanceSource: StatusSource = StatusSource.ACTUAL, + val stakingBalanceSource: StatusSource = StatusSource.ACTUAL, ) { val total: StatusSource by lazy { - listOf(networkSource, quoteSource, yieldBalanceSource).getResultStatusSource() + listOf(networkSource, quoteSource, stakingBalanceSource).getResultStatusSource() } } @@ -163,7 +160,7 @@ data class CryptoCurrencyStatus( override val fiatAmount: SerializedBigDecimal, override val fiatRate: SerializedBigDecimal, override val priceChange: SerializedBigDecimal, - override val yieldBalance: YieldBalance?, + override val stakingBalance: StakingBalance?, override val yieldSupplyStatus: YieldSupplyStatus?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, @@ -191,7 +188,7 @@ data class CryptoCurrencyStatus( override val fiatAmount: SerializedBigDecimal?, override val fiatRate: SerializedBigDecimal?, override val priceChange: SerializedBigDecimal?, - override val yieldBalance: YieldBalance?, + override val stakingBalance: StakingBalance?, override val yieldSupplyStatus: YieldSupplyStatus?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, @@ -213,7 +210,7 @@ data class CryptoCurrencyStatus( @Serializable data class NoQuote( override val amount: SerializedBigDecimal, - override val yieldBalance: YieldBalance?, + override val stakingBalance: StakingBalance?, override val yieldSupplyStatus: YieldSupplyStatus?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/P2PStakingAccount.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/P2PStakingAccount.kt new file mode 100644 index 0000000000..edc489e496 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/P2PStakingAccount.kt @@ -0,0 +1,37 @@ +package com.tangem.domain.models.staking + +import com.tangem.domain.models.serialization.SerializedBigDecimal +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable + +/** P2P.org staking account */ +@Serializable +data class P2PStakingAccount( + val delegatorAddress: String, + val vaultAddress: String, + val stake: P2PStake, + val availableToUnstake: SerializedBigDecimal, + val availableToWithdraw: SerializedBigDecimal, + val exitQueue: P2PExitQueue, +) + +@Serializable +data class P2PStake( + val assets: SerializedBigDecimal, + val totalEarnedAssets: SerializedBigDecimal, +) + +@Serializable +data class P2PExitQueue( + val total: SerializedBigDecimal, + val requests: List, +) + +@Serializable +data class P2PExitRequest( + val ticket: String, + val totalAssets: SerializedBigDecimal, + val timestamp: Instant, + val withdrawalTimestamp: Instant, + val isClaimable: Boolean, +) \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingBalance.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingBalance.kt new file mode 100644 index 0000000000..a8493d22f4 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingBalance.kt @@ -0,0 +1,101 @@ +package com.tangem.domain.models.staking + +import com.tangem.domain.models.StatusSource +import kotlinx.serialization.Serializable +import java.math.BigDecimal + +/** + * Staking balance facade covering StakeKit and P2P balances + */ +@Serializable +sealed interface StakingBalance { + + val stakingId: StakingID + val source: StatusSource + + val totalStaked: BigDecimal + val totalRewards: BigDecimal? + val unstakingAmount: BigDecimal? + val withdrawableAmount: BigDecimal? + + @Serializable + sealed interface Data : StakingBalance { + + @Serializable + data class StakeKit( + override val stakingId: StakingID, + override val source: StatusSource, + val balance: YieldBalanceItem, + ) : Data { + + override val totalStaked: BigDecimal + get() = balance.items + .filter { it.type == BalanceType.STAKED } + .sumOf { it.amount } + + override val totalRewards: BigDecimal + get() = balance.items + .filter { it.type == BalanceType.REWARDS } + .sumOf { it.amount } + + override val unstakingAmount: BigDecimal + get() = balance.items + .filter { it.type == BalanceType.UNSTAKING || it.type == BalanceType.UNLOCKING } + .sumOf { it.amount } + + override val withdrawableAmount: BigDecimal + get() = balance.items + .filter { it.type == BalanceType.UNSTAKED } + .sumOf { it.amount } + } + + @Serializable + data class P2P( + override val stakingId: StakingID, + override val source: StatusSource, + val account: P2PStakingAccount, + ) : Data { + + override val totalStaked: BigDecimal + get() = account.stake.assets + + override val totalRewards: BigDecimal + get() = account.stake.totalEarnedAssets + + override val unstakingAmount: BigDecimal + get() = account.exitQueue.total + + override val withdrawableAmount: BigDecimal + get() = account.availableToWithdraw + } + } + + @Serializable + data class Empty( + override val stakingId: StakingID, + override val source: StatusSource, + ) : StakingBalance { + override val totalStaked: BigDecimal get() = BigDecimal.ZERO + override val totalRewards: BigDecimal? get() = null + override val unstakingAmount: BigDecimal? get() = null + override val withdrawableAmount: BigDecimal? get() = null + } + + @Serializable + data class Error(override val stakingId: StakingID) : StakingBalance { + override val source: StatusSource get() = StatusSource.ACTUAL + override val totalStaked: BigDecimal get() = BigDecimal.ZERO + override val totalRewards: BigDecimal? get() = null + override val unstakingAmount: BigDecimal? get() = null + override val withdrawableAmount: BigDecimal? get() = null + } + + fun copySealed(source: StatusSource): StakingBalance { + return when (this) { + is Data.StakeKit -> copy(source = source) + is Data.P2P -> copy(source = source) + is Empty -> copy(source = source) + is Error -> this + } + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingID.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingID.kt index 5ed3a0b3ce..64c8249582 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingID.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingID.kt @@ -3,4 +3,7 @@ package com.tangem.domain.models.staking import kotlinx.serialization.Serializable @Serializable -data class StakingID(val integrationId: String, val address: String) \ No newline at end of file +data class StakingID( + val integrationId: String, + val address: String, +) \ No newline at end of file diff --git a/domain/networks/detekt-baseline-main.xml b/domain/networks/detekt-baseline-main.xml new file mode 100644 index 0000000000..eaf966bb38 --- /dev/null +++ b/domain/networks/detekt-baseline-main.xml @@ -0,0 +1,8 @@ + + + + + UnnecessaryAbstractClass:MultiNetworkStatusSupplier.kt$MultiNetworkStatusSupplier$MultiNetworkStatusSupplier + UnnecessaryAbstractClass:SingleNetworkStatusSupplier.kt$SingleNetworkStatusSupplier$SingleNetworkStatusSupplier + + diff --git a/domain/news/src/main/java/com/tangem/domain/news/repository/NewsRepository.kt b/domain/news/src/main/java/com/tangem/domain/news/repository/NewsRepository.kt index c8ab6c6cd6..7db6669b0d 100644 --- a/domain/news/src/main/java/com/tangem/domain/news/repository/NewsRepository.kt +++ b/domain/news/src/main/java/com/tangem/domain/news/repository/NewsRepository.kt @@ -38,12 +38,12 @@ interface NewsRepository { suspend fun fetchDetailedArticles(newsIds: Collection, language: String?) /** - * Returns list of trending news by limit and with correct locale. + * Fetch list of trending news by limit and with correct locale and store it in runtime data store. * * @param limit * @param language current device locale */ - suspend fun getTrendingNews(limit: Int, language: String?): List + suspend fun getTrendingNews(limit: Int, language: String?) /** * Observes trending news with runtime viewed flag support. diff --git a/domain/news/src/main/java/com/tangem/domain/news/usecase/FetchTrendingNewsUseCase.kt b/domain/news/src/main/java/com/tangem/domain/news/usecase/FetchTrendingNewsUseCase.kt new file mode 100644 index 0000000000..319f111625 --- /dev/null +++ b/domain/news/src/main/java/com/tangem/domain/news/usecase/FetchTrendingNewsUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.news.usecase + +import arrow.core.Either +import com.tangem.domain.news.repository.NewsRepository +import java.util.Locale + +/** + * Fetches trending news to store it in runtime data store. + */ + +class FetchTrendingNewsUseCase(private val newsRepository: NewsRepository) { + + suspend operator fun invoke(): Either = Either.catch { + newsRepository.getTrendingNews( + limit = LIMIT_FOR_TRENDING_NEWS, + language = Locale.getDefault().language, + ) + } + + companion object { + private const val LIMIT_FOR_TRENDING_NEWS = 10 + } +} \ No newline at end of file diff --git a/domain/nft/build.gradle.kts b/domain/nft/build.gradle.kts index e7b3973752..4e39ec51eb 100644 --- a/domain/nft/build.gradle.kts +++ b/domain/nft/build.gradle.kts @@ -17,7 +17,7 @@ dependencies { // region Project – Domain implementation(projects.domain.core) - implementation(projects.domain.account.status) + implementation(projects.domain.account) implementation(projects.domain.models) implementation(projects.domain.networks) implementation(projects.domain.nft.models) diff --git a/domain/nft/models/detekt-baseline-main.xml b/domain/nft/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..526b4c8eb4 --- /dev/null +++ b/domain/nft/models/detekt-baseline-main.xml @@ -0,0 +1,9 @@ + + + + + MultilineLambdaItParameter:NFTCollections.kt${ it.content is NFTCollections.Content.Error || it.content is NFTCollections.Content.Collections && it.content.source == StatusSource.ONLY_CACHE } + MultilineLambdaItParameter:NFTCollections.kt${ val content = it.content content is NFTCollections.Content.Collections && content.collections.isNullOrEmpty() } + MultilineLambdaItParameter:NFTCollections.kt${ val content = it.content content is NFTCollections.Content.Collections && content.source != StatusSource.CACHE } + + diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/DisableWalletNFTUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/DisableWalletNFTUseCase.kt index c671f030a9..04e223a470 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/DisableWalletNFTUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/DisableWalletNFTUseCase.kt @@ -1,15 +1,16 @@ package com.tangem.domain.nft +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.nft.utils.NFTCleaner import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.wallets.repository.WalletsRepository class DisableWalletNFTUseCase( private val walletsRepository: WalletsRepository, - private val nftRepository: NFTRepository, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, + private val nftCleaner: NFTCleaner, ) { suspend operator fun invoke(userWalletId: UserWalletId) { @@ -20,7 +21,7 @@ class DisableWalletNFTUseCase( ) .orEmpty() - val networks = currencies.map { it.network } - nftRepository.clearCache(userWalletId, networks) + val networks = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::network) + nftCleaner(userWalletId, networks) } } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt index e7894f68c1..db4c13cacc 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt @@ -1,9 +1,8 @@ package com.tangem.domain.nft 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.models.account.Account -import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.nft.models.NFTCollections @@ -16,7 +15,7 @@ import kotlinx.coroutines.flow.* class GetNFTCollectionsUseCase( private val currenciesRepository: CurrenciesRepository, private val nftRepository: NFTRepository, - private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val singleAccountListSupplier: SingleAccountListSupplier, private val accountsFeatureToggles: AccountsFeatureToggles, ) { @@ -33,13 +32,17 @@ class GetNFTCollectionsUseCase( } } + @OptIn(ExperimentalCoroutinesApi::class) fun invokeForAccounts(userWalletId: UserWalletId): Flow { - fun AccountStatus.flowOfNFTCollections(): Flow>> = - nftCollections(userWalletId, this.flattenCurrencies().map { it.currency }) - .map { nfts -> this.account to nfts } + fun Account.flowOfNFTCollections(): Flow>> { + val currencies = (this as? Account.CryptoPortfolio)?.cryptoCurrencies.orEmpty() - return singleAccountStatusListSupplier(userWalletId) - .mapLatest { statusList -> statusList.accountStatuses.map { it.flowOfNFTCollections() } } + return nftCollections(userWalletId = userWalletId, cryptoCurrencies = currencies.toList()) + .map { nfts -> this to nfts } + } + + return singleAccountListSupplier(userWalletId) + .mapLatest { statusList -> statusList.accounts.map { it.flowOfNFTCollections() } } .flatMapLatest { flows -> combine(flows) { WalletNFTCollections(it.toMap()) } } } diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt index e3f8d2c71e..3332f38965 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt @@ -1,12 +1,14 @@ package com.tangem.domain.nft -import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.models.PortfolioId +import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.nft.models.NFTNetworks import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.tokens.repository.CurrenciesRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest @@ -14,19 +16,25 @@ import kotlinx.coroutines.flow.mapNotNull class GetNFTNetworksUseCase( private val currenciesRepository: CurrenciesRepository, - private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val singleAccountListSupplier: SingleAccountListSupplier, private val nftRepository: NFTRepository, ) { + @OptIn(ExperimentalCoroutinesApi::class) operator fun invoke(portfolioId: PortfolioId): Flow = when (portfolioId) { - is PortfolioId.Account -> singleAccountStatusListSupplier(portfolioId.userWalletId) - .map { it.accountStatuses } - .mapNotNull { accountStatuses -> accountStatuses.find { it.account.accountId == portfolioId.accountId } } - .map { accountStatus -> accountStatus.flattenCurrencies().map { it.currency } } - .mapLatest { it.toNFTNetworks(portfolioId.userWalletId) } - is PortfolioId.Wallet -> + is PortfolioId.Account -> { + singleAccountListSupplier(portfolioId.userWalletId) + .mapNotNull { accountList -> + val account = accountList.accounts.find { it.accountId == portfolioId.accountId } + + (account as? Account.CryptoPortfolio)?.cryptoCurrencies?.toList() + } + .mapLatest { it.toNFTNetworks(portfolioId.userWalletId) } + } + is PortfolioId.Wallet -> { currenciesRepository .getWalletCurrenciesUpdates(portfolioId.userWalletId) .map { cryptoCurrencies -> cryptoCurrencies.toNFTNetworks(portfolioId.userWalletId) } + } } private suspend fun List.toNFTNetworks(userWalletId: UserWalletId): NFTNetworks { diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt index a1aaa94941..86e2e2ac34 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt @@ -6,12 +6,12 @@ import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.nft.utils.NFTCleaner import com.tangem.domain.tokens.repository.CurrenciesRepository import kotlinx.coroutines.flow.* class ObserveAndClearNFTCacheIfNeedUseCase( - private val nftRepository: NFTRepository, + private val nftCleaner: NFTCleaner, private val currenciesRepository: CurrenciesRepository, private val accountsFeatureToggles: AccountsFeatureToggles, private val singleAccountListSupplier: SingleAccountListSupplier, @@ -26,7 +26,7 @@ class ObserveAndClearNFTCacheIfNeedUseCase( .distinctUntilChanged() .onEach { removedNetworks -> if (removedNetworks.isNotEmpty()) { - nftRepository.clearCache(userWalletId, removedNetworks.toList()) + nftCleaner(userWalletId, removedNetworks) } } diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/analytics/NFTAnalyticsEvent.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/analytics/NFTAnalyticsEvent.kt index 7ccdadbd7d..ade188cf05 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/analytics/NFTAnalyticsEvent.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/analytics/NFTAnalyticsEvent.kt @@ -1,6 +1,7 @@ package com.tangem.domain.nft.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.COLLECTIONS import com.tangem.core.analytics.models.AnalyticsParam.Key.NFT @@ -18,7 +19,7 @@ sealed class NFTAnalyticsEvent( ) { data class NFTListScreenOpened( - val state: State, + val state: AnalyticsParam.EmptyFull, val collectionsCount: Int, val allAssetsCount: Int, val noCollectionAssetsCount: Int, @@ -30,15 +31,10 @@ sealed class NFTAnalyticsEvent( put(NFT, allAssetsCount.toString()) put(NO_COLLECTION, noCollectionAssetsCount.toString()) }, - ) { - enum class State(val value: String) { - Empty("Empty"), - Full("Full"), - } - } + ) object Receive { - data object ScreenOpened : NFTAnalyticsEvent(event = "Receive NFT Screen Opened") + class ScreenOpened : NFTAnalyticsEvent(event = "Receive NFT Screen Opened") data class BlockchainChosen( private val blockchain: String, @@ -67,9 +63,9 @@ sealed class NFTAnalyticsEvent( }, ) - data object ButtonReadMore : NFTAnalyticsEvent(event = "Button - Read More") - data object ButtonSeeAll : NFTAnalyticsEvent(event = "Button - See All") - data object ButtonExplore : NFTAnalyticsEvent(event = "Button - Explore") - data object ButtonSend : NFTAnalyticsEvent(event = "Button - Send") + class ButtonReadMore : NFTAnalyticsEvent(event = "Button - Read More") + class ButtonSeeAll : NFTAnalyticsEvent(event = "Button - See All") + class ButtonExplore : NFTAnalyticsEvent(event = "Button - Explore") + class ButtonSend : NFTAnalyticsEvent(event = "Button - Send") } } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt index 52a74367a2..eea9a7acd3 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt @@ -2,11 +2,11 @@ package com.tangem.domain.nft.repository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.nft.models.NFTCollections import com.tangem.domain.nft.models.NFTSalePrice -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow interface NFTRepository { @@ -32,6 +32,4 @@ interface NFTRepository { suspend fun getNFTSupportedNetworks(userWalletId: UserWalletId): List suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String? - - suspend fun clearCache(userWalletId: UserWalletId, networks: List) } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/utils/NFTCleaner.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/utils/NFTCleaner.kt new file mode 100644 index 0000000000..f7b993b10c --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/utils/NFTCleaner.kt @@ -0,0 +1,30 @@ +package com.tangem.domain.nft.utils + +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Cleans up NFT data for a given user wallet and network(s). + * +[REDACTED_AUTHOR] + */ +interface NFTCleaner { + + /** + * Cleans up NFT data for a given user wallet and single network. + * + * @param userWalletId the user wallet id + * @param network the network to clean up + */ + suspend operator fun invoke(userWalletId: UserWalletId, network: Network) { + invoke(userWalletId = userWalletId, networks = setOf(network)) + } + + /** + * Cleans up NFT data for a given user wallet and multiple networks. + * + * @param userWalletId the user wallet id + * @param networks the set of networks to clean up + */ + suspend operator fun invoke(userWalletId: UserWalletId, networks: Set) +} \ No newline at end of file diff --git a/domain/onboarding/detekt-baseline-main.xml b/domain/onboarding/detekt-baseline-main.xml new file mode 100644 index 0000000000..f6f53634a7 --- /dev/null +++ b/domain/onboarding/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + SuspendFunSwallowedCancellation:WasTwinsOnboardingShownUseCase.kt$WasTwinsOnboardingShownUseCase$runCatching + + diff --git a/domain/onramp/build.gradle.kts b/domain/onramp/build.gradle.kts index 7823e8dcea..cd1f511092 100644 --- a/domain/onramp/build.gradle.kts +++ b/domain/onramp/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { api(projects.domain.core) api(projects.domain.settings) implementation(deps.kotlin.serialization) + implementation(projects.domain.promo) /** Tests */ testImplementation(deps.test.coroutine) diff --git a/domain/onramp/detekt-baseline-main.xml b/domain/onramp/detekt-baseline-main.xml new file mode 100644 index 0000000000..59b9829455 --- /dev/null +++ b/domain/onramp/detekt-baseline-main.xml @@ -0,0 +1,12 @@ + + + + + MaxChainedCallsOnSameLine:GetOnrampOffersUseCase.kt$GetOnrampOffersUseCase$offer.quote.paymentMethod.type.getProcessingSpeed().speed + MultilineLambdaItParameter:GetOnrampQuotesUseCase.kt$GetOnrampQuotesUseCase${ when (it) { is OnrampQuote.Data -> it.toAmount.value is OnrampQuote.Error -> null // negative difference to sort both when data and unavailable is present is OnrampQuote.AmountError -> { when (val error = it.error) { is OnrampError.AmountError.TooSmallError -> it.fromAmount.value - error.requiredAmount is OnrampError.AmountError.TooBigError -> error.requiredAmount - it.fromAmount.value } } } } + NamedArguments:GetOnrampOffersUseCase.kt$GetOnrampOffersUseCase$determineAdvantages( recentOffer, bestRateOffer, fastestOffer, isSingleOffer, ) + UnnecessaryLet:OnrampAnalyticsEvent.kt$OnrampAnalyticsEvent.Errors$let { put(PAYMENT_METHOD, paymentMethod) } + UnnecessaryLet:OnrampAnalyticsEvent.kt$OnrampAnalyticsEvent.Errors$let { put(PROVIDER, providerName) } + UseEmptyCounterpart:OnrampAnalyticsEvent.kt$OnrampAnalyticsEvent$mapOf() + + diff --git a/domain/onramp/models/detekt-baseline-main.xml b/domain/onramp/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..b479d48536 --- /dev/null +++ b/domain/onramp/models/detekt-baseline-main.xml @@ -0,0 +1,10 @@ + + + + + BooleanPropertyNaming:OnrampCountry.kt$OnrampCountry$val onrampAvailable: Boolean + ObjectExtendsThrowable:OnrampPairsError.kt$OnrampPairsError$PairsNotFound : OnrampPairsError + ObjectExtendsThrowable:OnrampRedirectError.kt$OnrampRedirectError$VerificationFailed : OnrampRedirectError + ObjectExtendsThrowable:OnrampRedirectError.kt$OnrampRedirectError$WrongRequestId : OnrampRedirectError + + diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt index 0b2aba9380..11049ae4d2 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampOffersUseCase.kt @@ -11,9 +11,11 @@ import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository import com.tangem.domain.onramp.utils.calculateRateDif import com.tangem.domain.onramp.utils.compareOffersByRateSpeedAndPriority +import com.tangem.domain.promo.PromoRepository import com.tangem.domain.settings.repositories.SettingsRepository import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map class GetOnrampOffersUseCase( @@ -21,14 +23,16 @@ class GetOnrampOffersUseCase( private val onrampTransactionRepository: OnrampTransactionRepository, private val errorResolver: OnrampErrorResolver, private val settingsRepository: SettingsRepository, + private val promoRepository: PromoRepository, ) { operator fun invoke(): EitherFlow> { return combine( onrampRepository.getQuotes(), onrampTransactionRepository.getAllTransactions(), - ) { quotes, transactions -> - processOffers(quotes, transactions) + flow { emit(promoRepository.isMoonpayPromoActive()) }, + ) { quotes, transactions, isMoonpayPromoActive -> + processOffers(quotes, transactions, isMoonpayPromoActive) } .map { offers -> offers.right() } .catch { throwable -> errorResolver.resolve(throwable).left() } @@ -37,6 +41,7 @@ class GetOnrampOffersUseCase( private suspend fun processOffers( quotes: List, transactions: List, + isMoonpayPromoActive: Boolean, ): List { val validQuotes = quotes.filterIsInstance() if (validQuotes.isEmpty()) return emptyList() @@ -57,7 +62,7 @@ class GetOnrampOffersUseCase( val recentOffer = findRecentOffer(offers, transactions) val bestRateOffer = findBestRateOffer(offers, isGooglePayAvailable) - val fastestOffer = findFastestOffer(offers, isGooglePayAvailable) + val fastestOffer = findFastestOffer(offers, isGooglePayAvailable, isMoonpayPromoActive) return buildOffersBlocks( recentOffer = recentOffer, @@ -85,8 +90,24 @@ class GetOnrampOffersUseCase( return offers.maxWithOrNull(offerComparator(isGooglePayAvailable)) } - private fun findFastestOffer(offers: List, isGooglePayAvailable: Boolean): OnrampOffer? { - val instantOffers = offers.filter { it.quote.paymentMethod.type.isInstant() } + private fun findFastestOffer( + offers: List, + isGooglePayAvailable: Boolean, + isMoonpayPromoActive: Boolean, + ): OnrampOffer? { + val moonpayPromoOffers = if (isMoonpayPromoActive) { + offers.filter { offer -> + offer.quote.provider.id == MOONPAY_PROMO_PROVIDER_ID && + offer.quote.paymentMethod.type == PaymentMethodType.GOOGLE_PAY + } + } else { + emptyList() + } + + val instantOffers = moonpayPromoOffers.ifEmpty { + offers.filter { it.quote.paymentMethod.type.isInstant() } + } + return if (instantOffers.isNotEmpty()) { instantOffers.maxWithOrNull(fastestOfferComparator(isGooglePayAvailable)) } else { @@ -287,4 +308,8 @@ class GetOnrampOffersUseCase( -> true } } + + private companion object { + const val MOONPAY_PROMO_PROVIDER_ID = "moonpay" + } } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt index b7da9150d6..d9d98eac03 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt @@ -30,7 +30,7 @@ sealed class OnrampAnalyticsEvent( ), ) - data object SelectCurrencyScreenOpened : OnrampAnalyticsEvent(event = "Currency Screen Opened") + class SelectCurrencyScreenOpened : OnrampAnalyticsEvent(event = "Currency Screen Opened") data class FiatCurrencyChosen( private val currency: String, @@ -39,11 +39,11 @@ sealed class OnrampAnalyticsEvent( params = mapOf("Currency Type" to currency), ) - data object CloseOnramp : OnrampAnalyticsEvent(event = "Button - Close") + class CloseOnramp : OnrampAnalyticsEvent(event = "Button - Close") - data object SettingsOpened : OnrampAnalyticsEvent(event = "Onramp Settings Screen Opened") + class SettingsOpened : OnrampAnalyticsEvent(event = "Onramp Settings Screen Opened") - data object SelectResidenceOpened : OnrampAnalyticsEvent(event = "Residence Screen Opened") + class SelectResidenceOpened : OnrampAnalyticsEvent(event = "Residence Screen Opened") data class OnResidenceChosen( private val residence: String, @@ -59,7 +59,7 @@ sealed class OnrampAnalyticsEvent( params = mapOf(RESIDENCE to residence), ) - data object OnResidenceChange : OnrampAnalyticsEvent(event = "Button - Change") + class OnResidenceChange : OnrampAnalyticsEvent(event = "Button - Change") data class OnResidenceConfirm( private val residence: String, @@ -68,7 +68,7 @@ sealed class OnrampAnalyticsEvent( params = mapOf(RESIDENCE to residence), ) - data object ProvidersScreenOpened : OnrampAnalyticsEvent(event = "Providers Screen Opened") + class ProvidersScreenOpened : OnrampAnalyticsEvent(event = "Providers Screen Opened") data class ProviderCalculated( private val providerName: String, @@ -83,7 +83,7 @@ sealed class OnrampAnalyticsEvent( ), ) - data object PaymentMethodsScreenOpened : OnrampAnalyticsEvent(event = "Payment Method Screen Opened") + class PaymentMethodsScreenOpened : OnrampAnalyticsEvent(event = "Payment Method Screen Opened") data class OnPaymentMethodChosen( private val paymentMethod: String, @@ -135,8 +135,8 @@ sealed class OnrampAnalyticsEvent( ), ) - data object MinAmountError : OnrampAnalyticsEvent(event = "Error - Min Amount") - data object MaxAmountError : OnrampAnalyticsEvent(event = "Error - Max Amount") + class MinAmountError : OnrampAnalyticsEvent(event = "Error - Min Amount") + class MaxAmountError : OnrampAnalyticsEvent(event = "Error - Max Amount") data class Errors( private val tokenSymbol: String, @@ -203,7 +203,7 @@ sealed class OnrampAnalyticsEvent( ), ) - data object AllOffersClicked : OnrampAnalyticsEvent( + class AllOffersClicked : OnrampAnalyticsEvent( event = "Button - All Offers", params = emptyMap(), ) diff --git a/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt index 660735a8e7..5f249f97a2 100644 --- a/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt +++ b/domain/onramp/src/test/kotlin/com/tangem/domain/onramp/GetOnrampOffersUseCaseTest.kt @@ -7,6 +7,7 @@ import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository +import com.tangem.domain.promo.PromoRepository import com.tangem.domain.settings.repositories.SettingsRepository import io.mockk.* import kotlinx.coroutines.flow.flowOf @@ -24,6 +25,7 @@ class GetOnrampOffersUseCaseTest { private val errorResolver: OnrampErrorResolver = mockk(relaxUnitFun = true) private val settingsRepository: SettingsRepository = mockk(relaxUnitFun = true) private val cryptoCurrencyId: CryptoCurrency.ID = mockk(relaxUnitFun = true) + private val promoRepository: PromoRepository = mockk(relaxUnitFun = true) private lateinit var useCase: GetOnrampOffersUseCase @@ -35,6 +37,7 @@ class GetOnrampOffersUseCaseTest { onrampTransactionRepository = onrampTransactionRepository, errorResolver = errorResolver, settingsRepository = settingsRepository, + promoRepository = promoRepository, ) } @@ -225,6 +228,7 @@ class GetOnrampOffersUseCaseTest { val transactions = emptyList() + coEvery { promoRepository.isMoonpayPromoActive() } returns false coEvery { settingsRepository.isGooglePayAvailability() } returns false coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) coEvery { onrampTransactionRepository.getAllTransactions() } returns flowOf( @@ -245,6 +249,107 @@ class GetOnrampOffersUseCaseTest { } } + @Test + fun `invoke should fallback to standard instant offers when promo is active but no Moonpay offers exist`() = + runTest { + val instantMethod = createMockPaymentMethod("gpay", "Google Pay", PaymentMethodType.GOOGLE_PAY) + val slowMethod = createMockPaymentMethod("bank", "Bank Transfer", PaymentMethodType.CARD) + val provider = createMockProvider("other", "Other Provider") + + val quotes = listOf( + createMockQuote(instantMethod, provider, BigDecimal("95.0")), + createMockQuote(slowMethod, provider, BigDecimal("100.0")), + ) + + val transactions = emptyList() + + coEvery { promoRepository.isMoonpayPromoActive() } returns true + coEvery { settingsRepository.isGooglePayAvailability() } returns true + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { onrampTransactionRepository.getAllTransactions() } returns flowOf(transactions) + + val result = useCase() + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(1) + + val recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended } + Truth.assertThat(recommendedBlock).isNotNull() + Truth.assertThat(recommendedBlock?.offers).hasSize(2) + + val fastestOffer = + recommendedBlock?.offers?.find { it.advantages == OnrampOfferAdvantages.Fastest } + Truth.assertThat(fastestOffer).isNotNull() + + when (val quote = fastestOffer?.quote) { + is OnrampQuote.Data -> { + Truth.assertThat(quote.provider.id).isNotEqualTo("moonpay") + Truth.assertThat(quote.paymentMethod.type).isEqualTo(PaymentMethodType.GOOGLE_PAY) + } + else -> Truth.assertThat(false).isTrue() + } + }, + ) + } + } + + @Test + fun `invoke should show Moonpay fastest offer when promo is active`() = runTest { + val moonpayGooglePayMethod = createMockPaymentMethod("moonpay-gpay", "Google Pay", PaymentMethodType.GOOGLE_PAY) + val otherGooglePayMethod = createMockPaymentMethod( + "other-gpay", + "Other Google Pay", + PaymentMethodType.GOOGLE_PAY, + ) + val slowMethod = createMockPaymentMethod("bank", "Bank Transfer", PaymentMethodType.CARD) + val moonpayProvider = createMockProvider("moonpay", "Moonpay") + val otherProvider = createMockProvider("other", "Other Provider") + + val quotes = listOf( + createMockQuote(moonpayGooglePayMethod, moonpayProvider, BigDecimal("100.0")), + createMockQuote(otherGooglePayMethod, otherProvider, BigDecimal("95.0")), + createMockQuote(slowMethod, otherProvider, BigDecimal("105.0")), + ) + + val transactions = emptyList() + + coEvery { promoRepository.isMoonpayPromoActive() } returns true + coEvery { settingsRepository.isGooglePayAvailability() } returns true + coEvery { onrampRepository.getQuotes() } returns flowOf(quotes) + coEvery { onrampTransactionRepository.getAllTransactions() } returns flowOf(transactions) + + val result = useCase() + + result.collect { either -> + Truth.assertThat(either.isRight()).isTrue() + either.fold( + ifLeft = { error -> Truth.assertThat(error).isNull() }, + ifRight = { offers -> + Truth.assertThat(offers).hasSize(1) + + val recommendedBlock = offers.find { it.category == OnrampOfferCategory.Recommended } + Truth.assertThat(recommendedBlock).isNotNull() + + val fastestOffer = recommendedBlock?.offers?.find { it.advantages == OnrampOfferAdvantages.Fastest } + Truth.assertThat(fastestOffer).isNotNull() + + when (val quote = fastestOffer?.quote) { + is OnrampQuote.Data -> { + Truth.assertThat(quote.provider.id).isEqualTo("moonpay") + Truth.assertThat(quote.paymentMethod.type).isEqualTo(PaymentMethodType.GOOGLE_PAY) + Truth.assertThat(quote.toAmount.value).isEqualTo(BigDecimal("100.0")) + } + else -> Truth.assertThat(false).isTrue() + } + }, + ) + } + } + private fun createMockPaymentMethod( id: String, name: String, diff --git a/domain/promo/detekt-baseline-main.xml b/domain/promo/detekt-baseline-main.xml new file mode 100644 index 0000000000..b913db3534 --- /dev/null +++ b/domain/promo/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + NullableBooleanCheck:GetStoryContentUseCase.kt$GetStoryContentUseCase$isFCAAllowed(id).firstOrNull() ?: false + + diff --git a/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt index 18d7a50ec4..6e7afbdb02 100644 --- a/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt +++ b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt @@ -16,9 +16,11 @@ interface PromoRepository { suspend fun setNeverToShowTokenPromo(promoId: PromoId) - suspend fun isMarketsStakingNotificationHideClicked(): Flow + fun isMarketsStakingNotificationHideClicked(): Flow suspend fun setMarketsStakingNotificationHideClicked() + + suspend fun isMoonpayPromoActive(): Boolean // endregion // region Stories diff --git a/domain/quotes/detekt-baseline-main.xml b/domain/quotes/detekt-baseline-main.xml new file mode 100644 index 0000000000..bb2982d875 --- /dev/null +++ b/domain/quotes/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + UnnecessaryAbstractClass:SingleQuoteStatusSupplier.kt$SingleQuoteStatusSupplier$SingleQuoteStatusSupplier + + diff --git a/domain/staking/detekt-baseline-debug.xml b/domain/staking/detekt-baseline-debug.xml index d84b9b39da..ced29d17bf 100644 --- a/domain/staking/detekt-baseline-debug.xml +++ b/domain/staking/detekt-baseline-debug.xml @@ -2,13 +2,12 @@ - CanBeNonNullable:StakingAnalyticsEvent.kt$StakingAnalyticsEvent$value: Any? MultilineLambdaItParameter:FetchStakingYieldBalanceUseCase.kt$FetchStakingYieldBalanceUseCase${ when (it) { is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$it")) StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() } return@either } MultilineLambdaItParameter:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase${ !it.isPending && action.amount < it.amount && it.type == BalanceType.STAKED && it.validatorAddress == action.validatorAddress } NamedArguments:GetConstructedStakingTransactionUseCase.kt$GetConstructedStakingTransactionUseCase$constructTransaction(networkId, fee, amount, transactionId) NullableToStringCall:StakingApyFlowUseCase.kt$StakingApyFlowUseCase$${yield.token.coinGeckoId} - UnnecessaryAbstractClass:MultiYieldBalanceSupplier.kt$MultiYieldBalanceSupplier$MultiYieldBalanceSupplier - UnnecessaryAbstractClass:SingleYieldBalanceSupplier.kt$SingleYieldBalanceSupplier$SingleYieldBalanceSupplier + UnnecessaryAbstractClass:MultiStakingBalanceSupplier.kt$MultiStakingBalanceSupplier$MultiStakingBalanceSupplier + UnnecessaryAbstractClass:SingleStakingBalanceSupplier.kt$SingleStakingBalanceSupplier$SingleStakingBalanceSupplier UseEmptyCounterpart:StakingAnalyticsEvent.kt$StakingAnalyticsEvent$mapOf() UseOrEmpty:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase$action.validatorAddress ?: action.validatorAddresses?.getOrNull(0) ?: "" diff --git a/domain/staking/models/detekt-baseline-main.xml b/domain/staking/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..865d3ce960 --- /dev/null +++ b/domain/staking/models/detekt-baseline-main.xml @@ -0,0 +1,19 @@ + + + + + BooleanPropertyNaming:P2PEthPoolStaking.kt$P2PEthPoolStaking.Metadata.Fee$val enabled: Boolean + BooleanPropertyNaming:P2PEthPoolStaking.kt$P2PEthPoolStaking.Status$val enter: Boolean + BooleanPropertyNaming:P2PEthPoolStaking.kt$P2PEthPoolStaking.Status$val exit: Boolean + BooleanPropertyNaming:StakingActionCommonType.kt$StakingActionCommonType.Enter$val skipEnterAmount: Boolean + BooleanPropertyNaming:StakingActionCommonType.kt$StakingActionCommonType.Exit$val partiallyUnstakeDisabled: Boolean + BooleanPropertyNaming:StakingActionCommonType.kt$StakingActionCommonType.Pending.Stake$val skipEnterAmount: Boolean + BooleanPropertyNaming:Yield.kt$AddressArgument$val required: Boolean + BooleanPropertyNaming:Yield.kt$Yield$val allValidatorsFull: Boolean = validators.all { it.status == Validator.ValidatorStatus.FULL } + BooleanPropertyNaming:Yield.kt$Yield.Metadata$val supportsMultipleValidators: Boolean? + BooleanPropertyNaming:Yield.kt$Yield.Metadata.Enabled$val enabled: Boolean + BooleanPropertyNaming:Yield.kt$Yield.Status$val enter: Boolean + BooleanPropertyNaming:Yield.kt$Yield.Status$val exit: Boolean? + BooleanPropertyNaming:Yield.kt$Yield.Validator$val preferred: Boolean + + diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingOption.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingOption.kt index 1a4563d0b5..60f9674e3f 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingOption.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingOption.kt @@ -39,12 +39,11 @@ sealed interface StakingOption { * P2P pooled staking option * Wraps P2P ETH Pool vault information */ - data class P2P(val vault: P2PEthPoolVault) : StakingOption { - override val integrationId: String = - "p2p-ethereum-pooled:${vault.vaultAddress}" - override val apy: SerializedBigDecimal = vault.apy + data class P2P(val vaults: List) : StakingOption { + override val integrationId: String = "p2p-ethereum-pooled" + override val apy: SerializedBigDecimal = vaults.maxOf { it.apy } override val token: YieldToken = createEthToken() - override val isAvailable: Boolean = !vault.isPrivate + override val isAvailable: Boolean = vaults.isNotEmpty() private fun createEthToken(): YieldToken { // TODO return YieldToken( diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolNetwork.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolNetwork.kt index 2321e39114..dfbed790f9 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolNetwork.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PEthPoolNetwork.kt @@ -11,6 +11,8 @@ enum class P2PEthPoolNetwork( val value: String, val displayName: String, val chainId: Int, + val stakingNetworkId: String, + val isTestnet: Boolean, ) { /** * Ethereum mainnet @@ -20,16 +22,20 @@ enum class P2PEthPoolNetwork( value = "mainnet", displayName = "Ethereum", chainId = 1, + stakingNetworkId = "ethereum", + isTestnet = false, ), /** - * Ethereum testnet (Holesky) + * Ethereum testnet (Hoodi) * Chain ID: 17000 */ TESTNET( value = "hoodi", - displayName = "Holesky Testnet", + displayName = "Hoodi Testnet", chainId = 17000, + stakingNetworkId = "ethereum/test", + isTestnet = true, ), ; diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PStakingConfig.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PStakingConfig.kt new file mode 100644 index 0000000000..52c0f311e8 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/ethpool/P2PStakingConfig.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.staking.model.ethpool + +/** + * Configuration for P2P Ethereum staking network. + * + * Change [USE_TESTNET] to switch between testnet and mainnet. + */ +object P2PStakingConfig { + + const val USE_TESTNET: Boolean = true + + val activeNetwork: P2PEthPoolNetwork + get() = if (USE_TESTNET) P2PEthPoolNetwork.TESTNET else P2PEthPoolNetwork.MAINNET +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt index 0308e969e5..a438d88cef 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt @@ -7,10 +7,10 @@ import arrow.core.right import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.stakekit.StakingError -import com.tangem.domain.staking.single.SingleYieldBalanceFetcher +import com.tangem.domain.staking.single.SingleStakingBalanceFetcher class FetchStakingYieldBalanceUseCase( - private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, + private val singleStakingBalanceFetcher: SingleStakingBalanceFetcher, private val stakingIdFactory: StakingIdFactory, ) { @@ -32,8 +32,8 @@ class FetchStakingYieldBalanceUseCase( return@either } - singleYieldBalanceFetcher( - params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), + singleStakingBalanceFetcher( + params = SingleStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), ) .mapLeft { StakingError.DomainError("$it") } .bind() diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt b/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt index 42da91d04f..e2950b602d 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt @@ -2,7 +2,6 @@ package com.tangem.domain.staking.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.domain.staking.analytics.StakingAnalyticsEvent.ButtonRewards.addIfValueIsNotNull import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.models.staking.action.StakingActionType @@ -24,11 +23,11 @@ sealed class StakingAnalyticsEvent( ), ) - data object WhatIsStaking : StakingAnalyticsEvent( + class WhatIsStaking : StakingAnalyticsEvent( event = "Link - What Is Staking", ) - data object AmountScreenOpened : StakingAnalyticsEvent( + class AmountScreenOpened : StakingAnalyticsEvent( event = "Amount Screen Opened", ) @@ -54,7 +53,7 @@ sealed class StakingAnalyticsEvent( ), ) - data object RewardScreenOpened : StakingAnalyticsEvent( + class RewardScreenOpened : StakingAnalyticsEvent( event = "Reward Screen Opened", ) @@ -67,7 +66,7 @@ sealed class StakingAnalyticsEvent( ), ) - data object ButtonMax : StakingAnalyticsEvent( + class ButtonMax : StakingAnalyticsEvent( event = "Button - Max", ) @@ -98,7 +97,7 @@ sealed class StakingAnalyticsEvent( ), ) - data object ButtonRewards : StakingAnalyticsEvent( + class ButtonRewards : StakingAnalyticsEvent( event = "Button - Rewards", ) @@ -112,9 +111,9 @@ sealed class StakingAnalyticsEvent( ), ) - data object ButtonShare : StakingAnalyticsEvent(event = "Button - Share") + class ButtonShare : StakingAnalyticsEvent(event = "Button - Share") - data object ButtonExplore : StakingAnalyticsEvent(event = "Button - Explore") + class ButtonExplore : StakingAnalyticsEvent(event = "Button - Explore") data class StakeKitApiError( val stakingError: StakingError.StakeKitApiError, @@ -145,12 +144,6 @@ sealed class StakingAnalyticsEvent( }, ) - fun MutableMap.addIfValueIsNotNull(key: String, value: Any?) { - if (value != null) { - put(key, value.toString()) - } - } - data class TransactionError( val errorCode: String, ) : StakingAnalyticsEvent( @@ -178,4 +171,11 @@ sealed class StakingAnalyticsEvent( enum class StakeScreenSource { Info, Amount, Confirmation, Validators, +} + +@Suppress("CanBeNonNullable") +fun MutableMap.addIfValueIsNotNull(key: String, value: Any?) { + if (value != null) { + put(key, value.toString()) + } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/fetcher/YieldBalanceFetcherParams.kt b/domain/staking/src/main/java/com/tangem/domain/staking/fetcher/StakingBalanceFetcherParams.kt similarity index 76% rename from domain/staking/src/main/java/com/tangem/domain/staking/fetcher/YieldBalanceFetcherParams.kt rename to domain/staking/src/main/java/com/tangem/domain/staking/fetcher/StakingBalanceFetcherParams.kt index e73c76dc8a..de89cdee9e 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/fetcher/YieldBalanceFetcherParams.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/fetcher/StakingBalanceFetcherParams.kt @@ -5,17 +5,17 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId /** - * Params for fetchers of yield balance + * Params for fetchers of staking balance * [REDACTED_AUTHOR] */ -sealed interface YieldBalanceFetcherParams { +sealed interface StakingBalanceFetcherParams { /** User wallet ID */ val userWalletId: UserWalletId /** - * Params for fetching multiple yield balances + * Params for fetching multiple staking balances * * @property userWalletId user wallet ID * @property currencyIdWithNetworkMap map of currency ID to network @@ -23,10 +23,10 @@ sealed interface YieldBalanceFetcherParams { data class Multi( override val userWalletId: UserWalletId, val currencyIdWithNetworkMap: Map, - ) : YieldBalanceFetcherParams + ) : StakingBalanceFetcherParams /** - * Params for fetching single yield balance + * Params for fetching single staking balance * * @property userWalletId user wallet ID * @property currencyId currency ID @@ -36,5 +36,5 @@ sealed interface YieldBalanceFetcherParams { override val userWalletId: UserWalletId, val currencyId: CryptoCurrency.ID, val network: Network, - ) : YieldBalanceFetcherParams + ) : StakingBalanceFetcherParams } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt index 3ce629bf6f..1bf895cbcc 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt @@ -5,6 +5,7 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toMigratedCoinId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.staking.model.ethpool.P2PStakingConfig /** * Represents a staking integration identifier. @@ -101,8 +102,10 @@ sealed interface StakingIntegrationID { enum class P2P : StakingIntegrationID { EthereumPooled { override val value: String = "p2p-ethereum-pooled" - override val blockchain: Blockchain = Blockchain.Ethereum - override val networkId: String = "ethereum" + override val blockchain: Blockchain + get() = if (P2PStakingConfig.USE_TESTNET) Blockchain.EthereumTestnet else Blockchain.Ethereum + override val networkId: String + get() = P2PStakingConfig.activeNetwork.stakingNetworkId }, } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiStakingBalanceFetcher.kt similarity index 75% rename from domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt rename to domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiStakingBalanceFetcher.kt index 041755dbc8..9657856acf 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiStakingBalanceFetcher.kt @@ -5,14 +5,14 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.staking.StakingID /** - * Fetcher of yields balances + * Fetcher of staking balances * [REDACTED_AUTHOR] */ -interface MultiYieldBalanceFetcher : FlowFetcher { +interface MultiStakingBalanceFetcher : FlowFetcher { /** - * Params for fetching multiple yield balances + * Params for fetching multiple staking balances * * @property userWalletId user wallet ID * @property stakingIds map of currency ID to network @@ -24,7 +24,7 @@ interface MultiYieldBalanceFetcher : FlowFetcher> { + + data class Params(val userWalletId: UserWalletId) + + interface Factory : FlowProducer.Factory +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiStakingBalanceSupplier.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiStakingBalanceSupplier.kt new file mode 100644 index 0000000000..106e390f01 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiStakingBalanceSupplier.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.staking.multi + +import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.models.staking.StakingBalance + +/** + * Supplier of all staking balances for selected wallet [MultiStakingBalanceProducer.Params] + * + * @property factory factory for creating [MultiStakingBalanceProducer] + * @property keyCreator key creator + * +[REDACTED_AUTHOR] + */ +abstract class MultiStakingBalanceSupplier( + override val factory: FlowProducer.Factory, + override val keyCreator: (MultiStakingBalanceProducer.Params) -> String, +) : FlowCachingSupplier>() \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt deleted file mode 100644 index 7b9357cb24..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.domain.staking.multi - -import com.tangem.domain.core.flow.FlowProducer -import com.tangem.domain.models.staking.YieldBalance -import com.tangem.domain.models.wallet.UserWalletId - -/** - * Producer of all yield balances for selected wallet [UserWalletId] - * -[REDACTED_AUTHOR] - */ -interface MultiYieldBalanceProducer : FlowProducer> { - - data class Params(val userWalletId: UserWalletId) - - interface Factory : FlowProducer.Factory -} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt deleted file mode 100644 index 6d6f106113..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.domain.staking.multi - -import com.tangem.domain.core.flow.FlowCachingSupplier -import com.tangem.domain.core.flow.FlowProducer -import com.tangem.domain.models.staking.YieldBalance - -/** - * Supplier of all yield balances for selected wallet [MultiYieldBalanceProducer.Params] - * - * @property factory factory for creating [MultiYieldBalanceProducer] - * @property keyCreator key creator - * -[REDACTED_AUTHOR] - */ -abstract class MultiYieldBalanceSupplier( - override val factory: FlowProducer.Factory, - override val keyCreator: (MultiYieldBalanceProducer.Params) -> String, -) : FlowCachingSupplier>() \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/P2PEthPoolRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/P2PEthPoolRepository.kt index fabb8176d1..8a116ff9b6 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/P2PEthPoolRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/P2PEthPoolRepository.kt @@ -2,7 +2,13 @@ package com.tangem.domain.staking.repositories import arrow.core.Either import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.staking.model.ethpool.* +import com.tangem.domain.staking.model.ethpool.P2PEthPoolAccount +import com.tangem.domain.staking.model.ethpool.P2PEthPoolBroadcastResult +import com.tangem.domain.staking.model.ethpool.P2PEthPoolNetwork +import com.tangem.domain.staking.model.ethpool.P2PEthPoolReward +import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx +import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault +import com.tangem.domain.staking.model.ethpool.P2PStakingConfig import com.tangem.domain.staking.model.stakekit.StakingError import kotlinx.coroutines.flow.Flow @@ -13,7 +19,7 @@ interface P2PEthPoolRepository { * * @param network P2P network (MAINNET or TESTNET) */ - suspend fun fetchVaults(network: P2PEthPoolNetwork = P2PEthPoolNetwork.MAINNET) + suspend fun fetchVaults(network: P2PEthPoolNetwork = P2PStakingConfig.activeNetwork) /** * Get list of available staking vaults @@ -22,7 +28,7 @@ interface P2PEthPoolRepository { * @return Either error or list of vaults with APY, capacity, fees */ suspend fun getVaults( - network: P2PEthPoolNetwork = P2PEthPoolNetwork.MAINNET, + network: P2PEthPoolNetwork = P2PStakingConfig.activeNetwork, ): Either> /** diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceFetcher.kt similarity index 71% rename from domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt rename to domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceFetcher.kt index 61a7ee90c6..f7ef742c1d 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceFetcher.kt @@ -5,14 +5,14 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.staking.StakingID /** - * Fetcher of yield balance + * Fetcher of staking balance * [REDACTED_AUTHOR] */ -interface SingleYieldBalanceFetcher : FlowFetcher { +interface SingleStakingBalanceFetcher : FlowFetcher { /** - * Params for fetching single yield balance + * Params for fetching single staking balance * * @property userWalletId user wallet ID * @property stakingId staking ID diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceProducer.kt similarity index 62% rename from domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt rename to domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceProducer.kt index 9d33907751..5bd51edae6 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceProducer.kt @@ -2,15 +2,15 @@ package com.tangem.domain.staking.single import com.tangem.domain.core.flow.FlowProducer import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance /** - * Producer of yield balance for selected wallet [UserWalletId] + * Producer of staking balance for selected wallet [UserWalletId] * [REDACTED_AUTHOR] */ -interface SingleYieldBalanceProducer : FlowProducer { +interface SingleStakingBalanceProducer : FlowProducer { data class Params( val userWalletId: UserWalletId, @@ -19,7 +19,7 @@ interface SingleYieldBalanceProducer : FlowProducer { override fun toString(): String { return """ - SingleYieldBalanceProducer.Params( + SingleStakingBalanceProducer.Params( userWalletId = $userWalletId, stakingId = $stakingId, ) @@ -27,5 +27,5 @@ interface SingleYieldBalanceProducer : FlowProducer { } } - interface Factory : FlowProducer.Factory + interface Factory : FlowProducer.Factory } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceSupplier.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceSupplier.kt new file mode 100644 index 0000000000..9474e0e172 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceSupplier.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.staking.single + +import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.models.staking.StakingBalance + +/** + * Supplier of staking balance for selected wallet [SingleStakingBalanceProducer.Params] + * + * @property factory factory for creating [SingleStakingBalanceProducer] + * @property keyCreator key creator + * +[REDACTED_AUTHOR] + */ +abstract class SingleStakingBalanceSupplier( + override val factory: FlowProducer.Factory, + override val keyCreator: (SingleStakingBalanceProducer.Params) -> String, +) : FlowCachingSupplier() \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt deleted file mode 100644 index 4d93e733a1..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.domain.staking.single - -import com.tangem.domain.core.flow.FlowCachingSupplier -import com.tangem.domain.core.flow.FlowProducer -import com.tangem.domain.models.staking.YieldBalance - -/** - * Supplier of yield balance for selected wallet [SingleYieldBalanceProducer.Params] - * - * @property factory factory for creating [SingleYieldBalanceProducer] - * @property keyCreator key creator - * -[REDACTED_AUTHOR] - */ -abstract class SingleYieldBalanceSupplier( - override val factory: FlowProducer.Factory, - override val keyCreator: (SingleYieldBalanceProducer.Params) -> String, -) : FlowCachingSupplier() \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/utils/StakingBalanceExt.kt b/domain/staking/src/main/java/com/tangem/domain/staking/utils/StakingBalanceExt.kt new file mode 100644 index 0000000000..75b52aa6f4 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/utils/StakingBalanceExt.kt @@ -0,0 +1,89 @@ +package com.tangem.domain.staking.utils + +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.StakingBalance +import com.tangem.lib.crypto.BlockchainUtils +import java.math.BigDecimal + +/** + * Provider-agnostic extension to get total balance including rewards. + * Works for both StakeKit and P2P providers. + * + * Returns sum of all staking-related balances including rewards + * (staked + unstaking + withdrawable + rewards). + * + * When [BlockchainUtils.isIncludeStakingTotalBalance] is false, the staked balance + * is already included in the main wallet balance, so we only return rewards. + */ +fun StakingBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String): BigDecimal { + return when (this) { + is StakingBalance.Data.StakeKit -> getTotalWithRewardsStakingBalanceStakeKit(blockchainId) + is StakingBalance.Data.P2P -> { + val rewards = totalRewards + if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId)) { + totalStaked + unstakingAmount + withdrawableAmount + rewards + } else { + rewards + } + } + } +} + +/** + * Provider-agnostic extension to get total staking balance excluding rewards. + * Works for both StakeKit and P2P providers. + * + * Returns sum of all staking-related balances (staked + unstaking + withdrawable) + * excluding rewards. + */ +fun StakingBalance.Data.getTotalStakingBalance(blockchainId: String): BigDecimal { + return when (this) { + is StakingBalance.Data.StakeKit -> getTotalStakingBalanceStakeKit(blockchainId) + is StakingBalance.Data.P2P -> totalStaked + unstakingAmount + withdrawableAmount + } +} + +/** + * StakeKit-specific extension to get total balance including rewards. + */ +private fun StakingBalance.Data.StakeKit.getTotalWithRewardsStakingBalanceStakeKit(blockchainId: String): BigDecimal { + return if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId = blockchainId)) { + balance.items.sumOf { it.amount } + } else { + getRewardStakingBalance() + } +} + +/** + * StakeKit-specific extension to get total staked balance excluding rewards. + */ +private fun StakingBalance.Data.StakeKit.getTotalStakingBalanceStakeKit(blockchainId: String): BigDecimal { + return if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId = blockchainId)) { + balance.items + .filterNot { it.type == BalanceType.REWARDS } + .sumOf { it.amount } + } else { + balance.items + .filterNot { it.type == BalanceType.REWARDS } + .sumOf { it.amount } - getRewardStakingBalance() + } +} + +/** + * StakeKit-specific extension to get reward balance. + */ +fun StakingBalance.Data.StakeKit.getRewardStakingBalance(): BigDecimal { + return balance.items + .filter { it.type == BalanceType.REWARDS } + .sumOf { it.amount } +} + +/** + * StakeKit-specific extension to get validators count. + */ +fun StakingBalance.Data.StakeKit.getValidatorsCount(): Int { + return balance.items + .filterNot { it.validatorAddress.isNullOrBlank() } + .distinctBy { it.validatorAddress } + .size +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt b/domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt deleted file mode 100644 index f6b6a96d42..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.domain.staking.utils - -import com.tangem.domain.models.staking.BalanceType -import com.tangem.domain.models.staking.YieldBalance -import com.tangem.lib.crypto.BlockchainUtils -import java.math.BigDecimal - -fun YieldBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String): BigDecimal { - return if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId = blockchainId)) { - balance.items.sumOf { it.amount } - } else { - getRewardStakingBalance() - } -} - -fun YieldBalance.Data.getTotalStakingBalance(blockchainId: String): BigDecimal { - return if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId = blockchainId)) { - balance.items - .filterNot { it.type == BalanceType.REWARDS } - .sumOf { it.amount } - } else { - balance.items - .filterNot { it.type == BalanceType.REWARDS } - .sumOf { it.amount } - getRewardStakingBalance() - } -} - -fun YieldBalance.Data.getRewardStakingBalance(): BigDecimal { - return balance.items - .filter { it.type == BalanceType.REWARDS } - .sumOf { it.amount } -} - -fun YieldBalance.Data.getValidatorsCount(): Int { - return balance.items - .filterNot { it.validatorAddress.isNullOrBlank() } - .distinctBy { it.validatorAddress } - .size -} \ No newline at end of file diff --git a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt index dc18df883b..996ea8cca0 100644 --- a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt +++ b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt @@ -149,7 +149,7 @@ internal class StakingIdFactoryTest { expected = createStakingId(integrationId = StakingIntegrationID.StakeKit.Coin.Cardano), ), CreateModel( - currencyId = createCurrencyId(blockchain = Blockchain.Ethereum), + currencyId = createCurrencyId(blockchain = StakingIntegrationID.P2P.EthereumPooled.blockchain), expected = createStakingId(integrationId = StakingIntegrationID.P2P.EthereumPooled), ), CreateModel( diff --git a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIntegrationIDTest.kt b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIntegrationIDTest.kt index ed60c5fd0c..745af46793 100644 --- a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIntegrationIDTest.kt +++ b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIntegrationIDTest.kt @@ -151,7 +151,7 @@ class StakingIntegrationIDTest { expected = StakingIntegrationID.StakeKit.Coin.Cardano, ), CreateModel( - currencyId = createCurrencyId(blockchain = Blockchain.Ethereum), + currencyId = createCurrencyId(blockchain = StakingIntegrationID.P2P.EthereumPooled.blockchain), expected = StakingIntegrationID.P2P.EthereumPooled, ), CreateModel( diff --git a/domain/tokens/detekt-baseline-debug.xml b/domain/tokens/detekt-baseline-debug.xml index f1b234f537..34d0f3eeb3 100644 --- a/domain/tokens/detekt-baseline-debug.xml +++ b/domain/tokens/detekt-baseline-debug.xml @@ -6,8 +6,6 @@ CanBeNonNullable:BaseActionsFactory.kt$BaseActionsFactory$requirementsDeferred: Deferred<AssetRequirementsCondition?>? CanBeNonNullable:CommonActionsFactory.kt$CommonActionsFactory$swapUnavailableReasonDeferred: Deferred<ScenarioUnavailabilityReason>? ExplicitCollectionElementAccessMethod:GetWalletTotalBalanceUseCase.kt$GetWalletTotalBalanceUseCase$walletBalanceCache.put(userWalletId, content) - MultilineLambdaItParameter:AddCryptoCurrenciesUseCase.kt$AddCryptoCurrenciesUseCase${ it.network.backendId == networkId && !it.isCustom && it.contractAddress.equals(contractAddress, true) } - MultilineLambdaItParameter:AddCryptoCurrenciesUseCase.kt$AddCryptoCurrenciesUseCase${ when (it) { is StakingIdFactory.Error.UnableToGetAddress -> raise(IllegalStateException("$it")) StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() } return@either } MultilineLambdaItParameter:BaseCurrencyStatusOperations.kt$BaseCurrencyStatusOperations${ singleQuoteStatusSupplier(params = SingleQuoteStatusProducer.Params(rawCurrencyId = it)) .firstOrNull() } MultilineLambdaItParameter:BaseCurrencyStatusOperations.kt$BaseCurrencyStatusOperations${ val exception = IllegalStateException("$it") Error.DataError(exception) } MultilineLambdaItParameter:FetchCurrencyStatusUseCase.kt$FetchCurrencyStatusUseCase${ when (it) { is StakingIdFactory.Error.UnableToGetAddress -> raise(IllegalStateException("$it")) StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() } return@either } @@ -20,7 +18,6 @@ MultilineLambdaItParameter:PriceChangeCalculator.kt$PriceChangeCalculator${ val weight = it.value.fiatAmount.orZero().divide(balance, 2, RoundingMode.HALF_UP) val priceChange = it.value.priceChange.orZero() weight * priceChange } MultilineLambdaItParameter:WalletBalanceFetcher.kt$WalletBalanceFetcher${ val stakingId = stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it) if (stakingId.isLeft { it is StakingIdFactory.Error.UnableToGetAddress }) { Timber.e("Unable to get staking ID for user wallet $userWalletId and currency ${it.id}") } stakingId } NamedArguments:ApplyTokenListSortingUseCase.kt$ApplyTokenListSortingUseCase$saveTokens(userWalletId, currencies, isGrouped, isSortedByBalance) - NamedArguments:CryptoCurrencyStatusFactory.kt$CryptoCurrencyStatusFactory$createStatus(currency, status, quoteStatus, maybeYieldBalance) NamedArguments:GetCurrencyWarningsUseCase.kt$GetCurrencyWarningsUseCase$combine( getCoinRelatedWarnings( userWalletId = userWalletId, networkId = currency.network.id, currencyId = currency.id, derivationPath = derivationPath, isSingleWalletWithTokens = isSingleWalletWithTokens, ), flowOf(currencyChecksRepository.getRentInfoWarning(userWalletId, currencyStatus)), flowOf(currencyChecksRepository.getExistentialDeposit(userWalletId, currency.network)), flowOf(currencyChecksRepository.getFeeResourceAmount(userWalletId, currency.network)), ) { coinRelatedWarnings, maybeRentWarning, maybeEdWarning, maybeFeeResource -> setOfNotNull( maybeRentWarning, maybeEdWarning?.let { getExistentialDepositWarning(currency, it) }, maybeFeeResource?.let { getFeeResourceWarning(it) }, * coinRelatedWarnings.toTypedArray(), getNetworkUnavailableWarning(currencyStatus), getNetworkNoAccountWarning(currencyStatus), getBeaconChainShutdownWarning(rawId = currency.network.id.rawId), getAssetRequirementsWarning(userWalletId = userWalletId, currency = currency), getMigrationFromMaticToPolWarning(currency), ) } NoNameShadowing:WalletBalanceFetcher.kt$WalletBalanceFetcher${ it is StakingIdFactory.Error.UnableToGetAddress } NullableBooleanCheck:GetCurrencyCheckUseCase.kt$GetCurrencyCheckUseCase$recipientAddress?.let { currencyChecksRepository.checkIfAccountFunded( userWalletId, network, recipientAddress, ) } ?: false diff --git a/domain/tokens/models/detekt-baseline-main.xml b/domain/tokens/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..80759d1dfb --- /dev/null +++ b/domain/tokens/models/detekt-baseline-main.xml @@ -0,0 +1,13 @@ + + + + + ObjectExtendsThrowable:RemoveCurrencyError.kt$RemoveCurrencyError$HasLinkedTokens : RemoveCurrencyError + UseEmptyCounterpart:PromoAnalyticsEvent.kt$PromoAnalyticsEvent$mapOf() + UseEmptyCounterpart:TokenExchangeAnalyticsEvent.kt$TokenExchangeAnalyticsEvent$mapOf() + UseEmptyCounterpart:TokenOnrampAnalyticsEvent.kt$TokenOnrampAnalyticsEvent$mapOf() + UseEmptyCounterpart:TokenReceiveAnalyticsEvent.kt$TokenReceiveAnalyticsEvent$mapOf() + UseEmptyCounterpart:TokenReceiveNewAnalyticsEvent.kt$TokenReceiveNewAnalyticsEvent$mapOf() + UseEmptyCounterpart:TokenScreenAnalyticsEvent.kt$TokenScreenAnalyticsEvent$mapOf() + + diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/PromoAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/PromoAnalyticsEvent.kt index 8eb3724038..3d3f0fbdb1 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/PromoAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/PromoAnalyticsEvent.kt @@ -31,22 +31,22 @@ sealed class PromoAnalyticsEvent( ), ) { sealed class BannerAction(val action: String) { - data object Clicked : BannerAction(action = "Clicked") - data object Closed : BannerAction(action = "Closed") + class Clicked : BannerAction(action = "Clicked") + class Closed : BannerAction(action = "Closed") } } // region visa waitlist promo - data object VisaWaitlistPromo : PromoAnalyticsEvent(event = "Visa Waitlist") + class VisaWaitlistPromo : PromoAnalyticsEvent(event = "Visa Waitlist") - data object VisaWaitlistPromoJoin : PromoAnalyticsEvent( + class VisaWaitlistPromoJoin : PromoAnalyticsEvent( event = "Button - Join Now", params = mapOf( "Program Name" to "Visa Waitlist", ), ) - data object VisaWaitlistPromoDismiss : PromoAnalyticsEvent( + class VisaWaitlistPromoDismiss : PromoAnalyticsEvent( event = "Button - Close", params = mapOf( "Program Name" to "Visa Waitlist", diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt index ffa78172b9..8261c75b29 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt @@ -30,7 +30,7 @@ sealed class TokenOnrampAnalyticsEvent( ), ) - data object GoToProvider : TokenOnrampAnalyticsEvent(event = "Button - Go To Provider") + class GoToProvider : TokenOnrampAnalyticsEvent(event = "Button - Go To Provider") data class NoticeKYC( val tokenSymbol: String, diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveNewAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveNewAnalyticsEvent.kt index cf752c7e38..5916c680aa 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveNewAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveNewAnalyticsEvent.kt @@ -15,7 +15,7 @@ sealed class TokenReceiveNewAnalyticsEvent( class ReceiveScreenOpened( token: String, blockchainName: String, - ensStatus: AnalyticsParam.EnsStatus, + ensStatus: AnalyticsParam.EmptyFull, ) : TokenReceiveNewAnalyticsEvent( event = "Receive Screen Opened", params = mapOf( diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt index 5c0c30da0d..41a0965407 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens.model.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_FROM import com.tangem.core.analytics.models.AnalyticsParam.Key.ACTION import com.tangem.core.analytics.models.AnalyticsParam.Key.BALANCE import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN @@ -58,12 +59,14 @@ sealed class TokenScreenAnalyticsEvent( token: String, blockchain: String, status: String?, + derivationIndex: Int? = null, ) : TokenScreenAnalyticsEvent( event = event, params = buildMap { put(TOKEN_PARAM, token) put(BLOCKCHAIN, blockchain) status?.let { put(STATUS, it) } + derivationIndex?.let { put(ACCOUNT_DERIVATION_FROM, it.toString()) } }, ) { @@ -71,11 +74,13 @@ sealed class TokenScreenAnalyticsEvent( token: String, blockchain: String, status: String?, + derivationIndex: Int? = null, ) : ButtonWithParams( event = "Button - Buy", token = token, status = status, blockchain = blockchain, + derivationIndex = derivationIndex, ) class ButtonSell( @@ -93,22 +98,26 @@ sealed class TokenScreenAnalyticsEvent( token: String, status: String, blockchain: String, + derivationIndex: Int? = null, ) : ButtonWithParams( event = "Button - Exchange", token = token, status = status, blockchain = blockchain, + derivationIndex = derivationIndex, ) class ButtonSend( token: String, status: String, blockchain: String, + derivationIndex: Int? = null, ) : ButtonWithParams( event = "Button - Send", token = token, status = status, blockchain = blockchain, + derivationIndex = derivationIndex, ) class ButtonReceive( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt index f6896dbad8..7473e2eb33 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt @@ -12,8 +12,9 @@ import com.tangem.domain.models.wallet.UserWalletId 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.single.SingleYieldBalanceFetcher +import com.tangem.domain.staking.single.SingleStakingBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -28,9 +29,10 @@ import kotlinx.coroutines.coroutineScope @Suppress("LongParameterList") class AddCryptoCurrenciesUseCase( private val currenciesRepository: CurrenciesRepository, + private val walletManagersFacade: WalletManagersFacade, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, + private val singleStakingBalanceFetcher: SingleStakingBalanceFetcher, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val stakingIdFactory: StakingIdFactory, ) { @@ -73,12 +75,14 @@ class AddCryptoCurrenciesUseCase( ) val currencyToAdd = currency.takeUnless(existingCurrencies::contains) ?: return@either - addCurrencies(userWalletId, currencyToAdd) + val addedCurrencies = addCurrencies(userWalletId, currencyToAdd) coroutineScope { + syncTokens(userWalletId, addedCurrencies) + awaitAll( async { refreshUpdatedNetworks(userWalletId, currencyToAdd, existingCurrencies) }, - async { refreshUpdatedYieldBalances(userWalletId, currencyToAdd) }, + async { refreshUpdatedStakingBalances(userWalletId, currencyToAdd) }, async { refreshUpdatedQuotes(currencyToAdd) }, ) } @@ -102,21 +106,23 @@ class AddCryptoCurrenciesUseCase( val foundToken = existingCurrencies .filterIsInstance() - .firstOrNull { - it.network.backendId == networkId && - !it.isCustom && - it.contractAddress.equals(contractAddress, true) + .firstOrNull { token -> + token.network.backendId == networkId && + !token.isCustom && + token.contractAddress.equals(contractAddress, true) } if (foundToken != null) { return@either foundToken } val tokenToAdd = createTokenCurrency(userWalletId, contractAddress, networkId) - addCurrencies(userWalletId, tokenToAdd) + val addedCurrencies = addCurrencies(userWalletId, tokenToAdd) coroutineScope { + syncTokens(userWalletId = userWalletId, addedCurrencies = addedCurrencies) + awaitAll( async { refreshUpdatedNetworks(userWalletId, tokenToAdd, existingCurrencies) }, - async { refreshUpdatedYieldBalances(userWalletId, tokenToAdd) }, + async { refreshUpdatedStakingBalances(userWalletId, tokenToAdd) }, async { refreshUpdatedQuotes(tokenToAdd) }, ) } @@ -124,6 +130,26 @@ class AddCryptoCurrenciesUseCase( tokenToAdd } + private suspend fun syncTokens(userWalletId: UserWalletId, addedCurrencies: List) { + createWalletManagers(userWalletId = userWalletId, currencies = addedCurrencies) + currenciesRepository.syncTokens(userWalletId) + } + + /** + * Creates wallet managers for the given [currencies] if they do not already exist. + * The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature. + * + * @param userWalletId The ID of the user's wallet. + * @param currencies The list of cryptocurrencies for which to create wallet managers. + */ + private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List) { + val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network) + + for (network in networks) { + walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network) + } + } + /** * Refreshes the network statuses for tokens that have corresponding coins in the * [existingCurrencies] list. @@ -149,11 +175,9 @@ class AddCryptoCurrenciesUseCase( networks = setOfNotNull(networksToUpdate, networkToUpdate), ), ) - - currenciesRepository.syncTokens(userWalletId) } - private suspend fun refreshUpdatedYieldBalances( + private suspend fun refreshUpdatedStakingBalances( userWalletId: UserWalletId, addedCurrency: CryptoCurrency, ): Either = either { @@ -162,17 +186,17 @@ class AddCryptoCurrenciesUseCase( currencyId = addedCurrency.id, network = addedCurrency.network, ) - .getOrElse { - when (it) { - is StakingIdFactory.Error.UnableToGetAddress -> raise(IllegalStateException("$it")) + .getOrElse { error -> + when (error) { + is StakingIdFactory.Error.UnableToGetAddress -> raise(IllegalStateException("$error")) StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() } return@either } - singleYieldBalanceFetcher( - params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), + singleStakingBalanceFetcher( + params = SingleStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), ) .bind() } @@ -205,12 +229,14 @@ class AddCryptoCurrenciesUseCase( ) } - private suspend fun Raise.addCurrencies(userWalletId: UserWalletId, currency: CryptoCurrency) { - catch( - { currenciesRepository.addCurrenciesCache(userWalletId, listOf(currency)) }, - ) { - raise(it) - } + private suspend fun Raise.addCurrencies( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): List { + return catch( + block = { currenciesRepository.addCurrenciesCache(userWalletId, listOf(currency)) }, + catch = ::raise, + ) } /** diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index 5c7ce2a444..a1598a3f5f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -12,7 +12,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.single.SingleYieldBalanceFetcher +import com.tangem.domain.staking.single.SingleStakingBalanceFetcher import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.repository.CurrenciesRepository import kotlinx.coroutines.async @@ -32,7 +32,7 @@ class FetchCurrencyStatusUseCase( private val currenciesRepository: CurrenciesRepository, private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, + private val singleStakingBalanceFetcher: SingleStakingBalanceFetcher, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val stakingIdFactory: StakingIdFactory, ) { @@ -149,8 +149,8 @@ class FetchCurrencyStatusUseCase( return@either } - singleYieldBalanceFetcher( - params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), + singleStakingBalanceFetcher( + params = SingleStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), ) .bind() } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt index 7fb8c805ca..6fc01f0c95 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt @@ -6,7 +6,7 @@ import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations internal fun CurrenciesStatusesOperations.Error.mapToCurrencyError(): CurrencyStatusError { return when (this) { is CurrenciesStatusesOperations.Error.DataError -> CurrencyStatusError.DataError(this.cause) - is CurrenciesStatusesOperations.Error.EmptyYieldBalances, + is CurrenciesStatusesOperations.Error.EmptyStakingBalances, is CurrenciesStatusesOperations.Error.EmptyNetworksStatuses, is CurrenciesStatusesOperations.Error.EmptyQuotes, is CurrenciesStatusesOperations.Error.EmptyCurrencies, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt index 5e4071d5b1..081d02f18f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt @@ -12,7 +12,7 @@ internal fun CurrenciesStatusesOperations.Error.mapToTokenListError(): TokenList is CurrenciesStatusesOperations.Error.EmptyCurrencies, is CurrenciesStatusesOperations.Error.EmptyAddresses, is CurrenciesStatusesOperations.Error.UnableToCreateCurrencyStatus, - is CurrenciesStatusesOperations.Error.EmptyYieldBalances, + is CurrenciesStatusesOperations.Error.EmptyStakingBalances, -> TokenListError.EmptyTokens } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index 76c726d9de..10043d51ca 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -10,8 +10,8 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier @@ -22,10 +22,10 @@ import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.model.isStakingSupported -import com.tangem.domain.staking.multi.MultiYieldBalanceProducer -import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier -import com.tangem.domain.staking.single.SingleYieldBalanceProducer -import com.tangem.domain.staking.single.SingleYieldBalanceSupplier +import com.tangem.domain.staking.multi.MultiStakingBalanceProducer +import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier +import com.tangem.domain.staking.single.SingleStakingBalanceProducer +import com.tangem.domain.staking.single.SingleStakingBalanceSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.error.TokenListError @@ -48,8 +48,8 @@ abstract class BaseCurrencyStatusOperations( private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, - private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, - private val multiYieldBalanceSupplier: MultiYieldBalanceSupplier, + private val singleStakingBalanceSupplier: SingleStakingBalanceSupplier, + private val multiStakingBalanceSupplier: MultiStakingBalanceSupplier, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val stakingIdFactory: StakingIdFactory, ) { @@ -83,7 +83,7 @@ abstract class BaseCurrencyStatusOperations( userWalletId: UserWalletId, currency: CryptoCurrency, includeQuotes: Boolean = true, - subscribeOnYieldBalance: Boolean = true, + subscribeOnStakingBalance: Boolean = true, ): Flow> { val rawCurrencyId = currency.id.rawCurrencyId @@ -104,7 +104,7 @@ abstract class BaseCurrencyStatusOperations( val isStakingSupported = currency.network.toBlockchain().isStakingSupported - val yieldBalanceFlow = if (isStakingSupported) { + val stakingBalanceFlow = if (isStakingSupported) { val stakingId = stakingIdFactory.create( userWalletId = userWalletId, currencyId = currency.id, @@ -113,19 +113,19 @@ abstract class BaseCurrencyStatusOperations( .getOrNull() stakingId?.let { - getYieldBalance(userWalletId = userWalletId, stakingId = it) + getStakingBalance(userWalletId = userWalletId, stakingId = it) } } else { null } - return if (subscribeOnYieldBalance && yieldBalanceFlow != null) { - combine(quoteFlow, statusFlow, yieldBalanceFlow) { maybeQuote, maybeNetworkStatus, maybeYieldBalance -> + return if (subscribeOnStakingBalance && stakingBalanceFlow != null) { + combine(quoteFlow, statusFlow, stakingBalanceFlow) { maybeQuote, maybeNetworkStatus, maybeStakingBalance -> currencyStatusProxyCreator.createCurrencyStatus( currency = currency, maybeQuoteStatus = maybeQuote, maybeNetworkStatus = maybeNetworkStatus, - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) } } else { @@ -134,7 +134,7 @@ abstract class BaseCurrencyStatusOperations( currency = currency, maybeQuoteStatus = maybeQuote, maybeNetworkStatus = maybeNetworkStatus, - maybeYieldBalance = null, + maybeStakingBalance = null, ) } } @@ -209,13 +209,13 @@ abstract class BaseCurrencyStatusOperations( .firstOrNull() .right() - val yieldBalances = getYieldBalanceSync(userWalletId, currency) + val stakingBalances = getStakingBalanceSync(userWalletId, currency) return currencyStatusProxyCreator.createCurrencyStatus( currency = currency, maybeQuoteStatus = quote, maybeNetworkStatus = networkStatuses, - maybeYieldBalance = yieldBalances, + maybeStakingBalance = stakingBalances, ) }, catch = { raise(Error.DataError(it)) }, @@ -245,7 +245,7 @@ abstract class BaseCurrencyStatusOperations( userWalletId = userWalletId, currency = currency, includeQuotes = includeQuotes, - subscribeOnYieldBalance = false, + subscribeOnStakingBalance = false, ) } @@ -271,13 +271,13 @@ abstract class BaseCurrencyStatusOperations( .orEmpty() .right() - val yieldBalances = getYieldBalancesSync(userWalletId, nonEmptyCurrencies) + val stakingBalances = getStakingBalancesSync(userWalletId, nonEmptyCurrencies) return currencyStatusProxyCreator.createCurrenciesStatuses( currencies = nonEmptyCurrencies, maybeQuotes = quotes, maybeNetworkStatuses = networkStatuses, - maybeYieldBalances = yieldBalances, + maybeStakingBalances = stakingBalances, ) }, catch = { raise(Error.DataError(it)) }, @@ -304,13 +304,13 @@ abstract class BaseCurrencyStatusOperations( .firstOrNull() .right() - val yieldBalances = getYieldBalanceSync(userWalletId, currency) + val stakingBalances = getStakingBalanceSync(userWalletId, currency) return currencyStatusProxyCreator.createCurrencyStatus( currency = currency, maybeQuoteStatus = quotes, maybeNetworkStatus = networkStatus, - maybeYieldBalance = yieldBalances, + maybeStakingBalance = stakingBalances, ) } @@ -338,16 +338,16 @@ abstract class BaseCurrencyStatusOperations( .bind() } - private fun getYieldBalance(userWalletId: UserWalletId, stakingId: StakingID): EitherFlow { - return singleYieldBalanceSupplier( - params = SingleYieldBalanceProducer.Params( + private fun getStakingBalance(userWalletId: UserWalletId, stakingId: StakingID): EitherFlow { + return singleStakingBalanceSupplier( + params = SingleStakingBalanceProducer.Params( userWalletId = userWalletId, stakingId = stakingId, ), ) - .map> { it.right() } + .map> { it.right() } .catch { emit(Error.DataError(it).left()) } - .onEmpty { emit(Error.EmptyYieldBalances.left()) } + .onEmpty { emit(Error.EmptyStakingBalances.left()) } } private fun getNetworkStatus(userWalletId: UserWalletId, network: Network): EitherFlow { @@ -389,32 +389,32 @@ abstract class BaseCurrencyStatusOperations( .bind() } - private suspend fun getYieldBalancesSync( + private suspend fun getStakingBalancesSync( userWalletId: UserWalletId, cryptoCurrencies: List, - ): Either> = either { + ): Either> = either { val stakingIds = cryptoCurrencies.mapNotNull { cryptoCurrency -> stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrency) .getOrNull() } - ensure(stakingIds.isNotEmpty()) { Error.EmptyYieldBalances } + ensure(stakingIds.isNotEmpty()) { Error.EmptyStakingBalances } - val balances = multiYieldBalanceSupplier.getSyncOrNull( - params = MultiYieldBalanceProducer.Params(userWalletId = userWalletId), + val balances = multiStakingBalanceSupplier.getSyncOrNull( + params = MultiStakingBalanceProducer.Params(userWalletId = userWalletId), ) .orEmpty() .filter { it.stakingId in stakingIds } - ensure(balances.isNotEmpty()) { Error.EmptyYieldBalances } + ensure(balances.isNotEmpty()) { Error.EmptyStakingBalances } balances } - private suspend fun getYieldBalanceSync( + private suspend fun getStakingBalanceSync( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): Either = either { + ): Either = either { val stakingId = stakingIdFactory.create(userWalletId, cryptoCurrency) .mapLeft { val exception = IllegalStateException("$it") @@ -422,14 +422,14 @@ abstract class BaseCurrencyStatusOperations( } .bind() - val yieldBalance = singleYieldBalanceSupplier.getSyncOrNull( - params = SingleYieldBalanceProducer.Params( + val yieldBalance = singleStakingBalanceSupplier.getSyncOrNull( + params = SingleStakingBalanceProducer.Params( userWalletId = userWalletId, stakingId = stakingId, ), ) - ensureNotNull(yieldBalance) { Error.EmptyYieldBalances } + ensureNotNull(yieldBalance) { Error.EmptyStakingBalances } } private suspend fun Raise.getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index fb20d04a34..a1561d6d2e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -16,7 +16,7 @@ import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.network.getAddress import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusProducer @@ -26,9 +26,9 @@ import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.model.StakingIntegrationID -import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier -import com.tangem.domain.staking.single.SingleYieldBalanceProducer -import com.tangem.domain.staking.single.SingleYieldBalanceSupplier +import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier +import com.tangem.domain.staking.single.SingleStakingBalanceProducer +import com.tangem.domain.staking.single.SingleStakingBalanceSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error @@ -46,8 +46,8 @@ class CachedCurrenciesStatusesOperations( private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, - private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, - multiYieldBalanceSupplier: MultiYieldBalanceSupplier, + private val singleStakingBalanceSupplier: SingleStakingBalanceSupplier, + multiStakingBalanceSupplier: MultiStakingBalanceSupplier, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val stakingIdFactory: StakingIdFactory, ) : BaseCurrencyStatusOperations( @@ -56,8 +56,8 @@ class CachedCurrenciesStatusesOperations( multiNetworkStatusSupplier = multiNetworkStatusSupplier, singleNetworkStatusSupplier = singleNetworkStatusSupplier, singleQuoteStatusSupplier = singleQuoteStatusSupplier, - singleYieldBalanceSupplier = singleYieldBalanceSupplier, - multiYieldBalanceSupplier = multiYieldBalanceSupplier, + singleStakingBalanceSupplier = singleStakingBalanceSupplier, + multiStakingBalanceSupplier = multiStakingBalanceSupplier, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, stakingIdFactory = stakingIdFactory, ) { @@ -95,7 +95,7 @@ class CachedCurrenciesStatusesOperations( currencies = currencies, maybeNetworkStatuses = null, maybeQuotes = null, - maybeYieldBalances = null, + maybeStakingBalances = null, isUpdating = true, ) @@ -109,13 +109,13 @@ class CachedCurrenciesStatusesOperations( fun createCurrenciesStatuses( maybeQuotes: Either>, maybeNetworkStatuses: Either>, - maybeYieldBalances: Either>, + maybeStakingBalances: Either>, isUpdating: Boolean, ) = createCurrenciesStatuses( currencies = currencies, maybeQuotes = maybeQuotes, maybeNetworkStatuses = maybeNetworkStatuses, - maybeYieldBalances = maybeYieldBalances, + maybeStakingBalances = maybeStakingBalances, isUpdating = isUpdating, ) @@ -166,13 +166,13 @@ class CachedCurrenciesStatusesOperations( currencies: NonEmptyList, maybeQuotes: Either>?, maybeNetworkStatuses: Either>?, - maybeYieldBalances: Either>?, + maybeStakingBalances: Either>?, isUpdating: Boolean, ): Lce> = lce { isLoading.set(isUpdating) val networksStatuses = maybeNetworkStatuses?.bindEither()?.toNonEmptySetOrNull() - val yieldBalances = maybeYieldBalances?.bindEither() + val stakingBalances = maybeStakingBalances?.bindEither() val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) { null } @@ -180,25 +180,25 @@ class CachedCurrenciesStatusesOperations( currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - val yieldBalance = findYieldBalanceOrNull(yieldBalances, currency, networkStatus) + val stakingBalance = findStakingBalanceOrNull(stakingBalances, currency, networkStatus) val currencyStatus = CryptoCurrencyStatusFactory.create( currency = currency, maybeNetworkStatus = networkStatus.toOption(), maybeQuoteStatus = quote.toOption(), - maybeYieldBalance = yieldBalance.toOption(), + maybeStakingBalance = stakingBalance.toOption(), ) currencyStatus } } - private fun findYieldBalanceOrNull( - yieldBalances: List?, + private fun findStakingBalanceOrNull( + stakingBalances: List?, currency: CryptoCurrency, networkStatus: NetworkStatus?, - ): YieldBalance? { - if (yieldBalances.isNullOrEmpty()) return null + ): StakingBalance? { + if (stakingBalances.isNullOrEmpty()) return null val supportedIntegration = StakingIntegrationID.create(currencyId = currency.id)?.value val address = networkStatus.getAddress() @@ -206,8 +206,8 @@ class CachedCurrenciesStatusesOperations( return if (supportedIntegration != null && address != null) { val stakingId = StakingID(integrationId = supportedIntegration, address = address) - yieldBalances.firstOrNull { it.stakingId == stakingId } - ?: YieldBalance.Error(stakingId = stakingId) + stakingBalances.firstOrNull { it.stakingId == stakingId } + ?: StakingBalance.Error(stakingId = stakingId) } else { null } @@ -302,9 +302,9 @@ class CachedCurrenciesStatusesOperations( private fun getYieldsBalancesUpdates( userWalletId: UserWalletId, cryptoCurrencies: Map, - ): EitherFlow> { + ): EitherFlow> { return channelFlow { - val state = MutableStateFlow(emptyList()) + val state = MutableStateFlow(emptyList()) val stakingIds = cryptoCurrencies.mapNotNullTo(hashSetOf()) { currencyWithAddress -> stakingIdFactory.create( @@ -316,8 +316,11 @@ class CachedCurrenciesStatusesOperations( stakingIds.onEach { stakingId -> launch { - singleYieldBalanceSupplier( - params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId), + singleStakingBalanceSupplier( + params = SingleStakingBalanceProducer.Params( + userWalletId = userWalletId, + stakingId = stakingId, + ), ) .onEach { balance -> state.update { loadedBalances -> diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactory.kt index 3ba3d65d4e..3c2726eb06 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactory.kt @@ -8,12 +8,12 @@ import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.quote.QuoteStatus -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.yield.supply.YieldSupplyStatus import java.math.BigDecimal /** - * Factory to create [CryptoCurrencyStatus] from [NetworkStatus], [QuoteStatus] and [YieldBalance]. + * Factory to create [CryptoCurrencyStatus] from [NetworkStatus], [QuoteStatus] and [StakingBalance]. * [REDACTED_AUTHOR] */ @@ -26,25 +26,25 @@ object CryptoCurrencyStatusFactory { get() = (this?.value as? QuoteStatus.Data)?.priceChange /** - * Creates [CryptoCurrencyStatus] from [NetworkStatus], [QuoteStatus] and [YieldBalance]. + * Creates [CryptoCurrencyStatus] from [NetworkStatus], [QuoteStatus] and [StakingBalance]. * * @param maybeNetworkStatus An optional network status containing blockchain information. * @param maybeQuoteStatus An optional quote status containing price information. - * @param maybeYieldBalance An optional yield balance containing staking information. + * @param maybeStakingBalance An optional staking balance containing staking information. */ fun create( currency: CryptoCurrency, maybeNetworkStatus: Option, maybeQuoteStatus: Option, - maybeYieldBalance: Option, + maybeStakingBalance: Option, ): CryptoCurrencyStatus { return CryptoCurrencyStatus( currency = currency, value = createStatus( currency = currency, maybeNetworkStatus = maybeNetworkStatus, - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, maybeQuoteStatus = maybeQuoteStatus, ), ) @@ -54,7 +54,7 @@ object CryptoCurrencyStatusFactory { currency: CryptoCurrency, maybeNetworkStatus: Option, maybeQuoteStatus: Option, - maybeYieldBalance: Option, + maybeStakingBalance: Option, ): CryptoCurrencyStatus.Value { val quoteStatus = maybeQuoteStatus.getOrNull() @@ -62,7 +62,12 @@ object CryptoCurrencyStatusFactory { is NetworkStatus.MissedDerivation -> createMissedDerivation(quoteStatus) is NetworkStatus.Unreachable -> createUnreachable(status, quoteStatus) is NetworkStatus.NoAccount -> createNoAccount(status, quoteStatus) - is NetworkStatus.Verified -> createStatus(currency, status, quoteStatus, maybeYieldBalance) + is NetworkStatus.Verified -> createStatus( + currency = currency, + status = status, + quoteStatus = quoteStatus, + maybeStakingBalance = maybeStakingBalance, + ) null -> CryptoCurrencyStatus.Loading } } @@ -106,7 +111,7 @@ object CryptoCurrencyStatusFactory { currency: CryptoCurrency, status: NetworkStatus.Verified, quoteStatus: QuoteStatus?, - maybeYieldBalance: Option, + maybeStakingBalance: Option, ): CryptoCurrencyStatus.Value { val amount = when (val amount = status.amounts[currency.id]) { is NetworkStatus.Amount.Loaded -> amount.value @@ -118,7 +123,7 @@ object CryptoCurrencyStatusFactory { } } - val yieldBalance = maybeYieldBalance.getOrNull(id = currency.id, address = status.address) + val stakingBalance = maybeStakingBalance.getOrNull(id = currency.id, address = status.address) if (currency is CryptoCurrency.Token && currency.isCustom) { return createCustom( @@ -126,7 +131,7 @@ object CryptoCurrencyStatusFactory { status = status, amount = amount, quoteStatus = quoteStatus, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, ) } @@ -138,7 +143,7 @@ object CryptoCurrencyStatusFactory { status = status, amount = amount, quoteStatus = quoteStatus, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, ) } is QuoteStatus.Data -> { @@ -147,7 +152,7 @@ object CryptoCurrencyStatusFactory { status = status, amount = amount, quoteStatus = quoteValue, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, ) } null -> CryptoCurrencyStatus.Loading @@ -166,7 +171,7 @@ object CryptoCurrencyStatusFactory { status: NetworkStatus.Verified, amount: BigDecimal, quoteStatus: QuoteStatus?, - yieldBalance: YieldBalance.Data?, + stakingBalance: StakingBalance.Data?, ): CryptoCurrencyStatus.Custom { return CryptoCurrencyStatus.Custom( amount = amount, @@ -176,11 +181,11 @@ object CryptoCurrencyStatusFactory { hasCurrentNetworkTransactions = status.hasCurrentNetworkTransactions(), pendingTransactions = status.getCurrentTransactions(id), networkAddress = status.address, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, yieldSupplyStatus = status.getYieldSupplyStatus(id), sources = CryptoCurrencyStatus.Sources( networkSource = status.source, - yieldBalanceSource = yieldBalance?.source ?: StatusSource.ACTUAL, + stakingBalanceSource = stakingBalance?.source ?: StatusSource.ACTUAL, quoteSource = quoteStatus?.value?.source ?: StatusSource.ACTUAL, ), ) @@ -191,18 +196,18 @@ object CryptoCurrencyStatusFactory { status: NetworkStatus.Verified, amount: BigDecimal, quoteStatus: QuoteStatus?, - yieldBalance: YieldBalance.Data?, + stakingBalance: StakingBalance.Data?, ): CryptoCurrencyStatus.NoQuote { return CryptoCurrencyStatus.NoQuote( amount = amount, hasCurrentNetworkTransactions = status.hasCurrentNetworkTransactions(), pendingTransactions = status.getCurrentTransactions(id), networkAddress = status.address, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, yieldSupplyStatus = status.getYieldSupplyStatus(id), sources = CryptoCurrencyStatus.Sources( networkSource = status.source, - yieldBalanceSource = yieldBalance?.source ?: StatusSource.ACTUAL, + stakingBalanceSource = stakingBalance?.source ?: StatusSource.ACTUAL, quoteSource = quoteStatus?.value?.source ?: StatusSource.ACTUAL, ), ) @@ -213,7 +218,7 @@ object CryptoCurrencyStatusFactory { status: NetworkStatus.Verified, amount: BigDecimal, quoteStatus: QuoteStatus.Data, - yieldBalance: YieldBalance.Data?, + stakingBalance: StakingBalance.Data?, ): CryptoCurrencyStatus.Loaded { return CryptoCurrencyStatus.Loaded( amount = amount, @@ -223,11 +228,11 @@ object CryptoCurrencyStatusFactory { hasCurrentNetworkTransactions = status.hasCurrentNetworkTransactions(), pendingTransactions = status.getCurrentTransactions(id), networkAddress = status.address, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, yieldSupplyStatus = status.getYieldSupplyStatus(id), sources = CryptoCurrencyStatus.Sources( networkSource = status.source, - yieldBalanceSource = yieldBalance?.source ?: StatusSource.ACTUAL, + stakingBalanceSource = stakingBalance?.source ?: StatusSource.ACTUAL, quoteSource = quoteStatus.source, ), ) @@ -243,20 +248,34 @@ object CryptoCurrencyStatusFactory { return yieldSupplyStatuses[id] } - private fun Option.getOrNull(id: CryptoCurrency.ID, address: NetworkAddress): YieldBalance.Data? { - val yieldBalance = this.getOrNull() as? YieldBalance.Data ?: return null + private fun Option.getOrNull( + id: CryptoCurrency.ID, + address: NetworkAddress, + ): StakingBalance.Data? { + return when (val stakingBalance = this.getOrNull()) { + is StakingBalance.Data.StakeKit -> { + val isCurrentAddressStaking = stakingBalance.stakingId.address == address.defaultAddress.value + val filteredTokenBalances = stakingBalance.balance.items.filter { + it.token.coinGeckoId == id.rawCurrencyId?.value + } - val isCurrentAddressStaking = yieldBalance.stakingId.address == address.defaultAddress.value - val filteredTokenBalances = yieldBalance.balance.items.filter { - it.token.coinGeckoId == id.rawCurrencyId?.value - } - - return if (isCurrentAddressStaking && filteredTokenBalances.isNotEmpty()) { - yieldBalance.copy( - balance = yieldBalance.balance.copy(items = filteredTokenBalances), - ) - } else { - null + if (isCurrentAddressStaking && filteredTokenBalances.isNotEmpty()) { + stakingBalance.copy( + balance = stakingBalance.balance.copy(items = filteredTokenBalances), + ) + } else { + null + } + } + is StakingBalance.Data.P2P -> { + // TODO p2p + val isCurrentAddressStaking = stakingBalance.stakingId.address == address.defaultAddress.value + if (isCurrentAddressStaking) stakingBalance else null + } + is StakingBalance.Empty, + is StakingBalance.Error, + null, + -> null } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 2679e1a047..a8f25caebe 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -16,6 +16,6 @@ class CurrenciesStatusesOperations { data class DataError(val cause: Throwable) : Error() - data object EmptyYieldBalances : Error() + data object EmptyStakingBalances : Error() } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFactory.kt index 7a84e34972..14fb123230 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFactory.kt @@ -6,7 +6,7 @@ import com.tangem.domain.models.TokensGroupType import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance @@ -123,10 +123,10 @@ object TokenListFactory { } private fun CryptoCurrencyStatus.calculateBalance(): BigDecimal { - val yieldBalance = value.yieldBalance as? YieldBalance.Data - val totalYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance(currency.network.rawId).orZero() - val totalFiatYieldBalance = totalYieldBalance.multiply(value.fiatRate.orZero()) + val stakingBalance = value.stakingBalance as? StakingBalance.Data + val totalStakingBalance = stakingBalance?.getTotalWithRewardsStakingBalance(currency.network.rawId).orZero() + val totalFiatStakingBalance = totalStakingBalance.multiply(value.fiatRate.orZero()) - return value.fiatAmount?.plus(totalFiatYieldBalance).orZero() + return value.fiatAmount?.plus(totalFiatStakingBalance).orZero() } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculator.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculator.kt index 47826a442d..db2f29b764 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculator.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculator.kt @@ -6,7 +6,7 @@ import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.getResultStatusSource -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.lib.crypto.BlockchainUtils @@ -153,17 +153,17 @@ object TotalFiatBalanceCalculator { } private fun CryptoCurrencyStatus.Loaded.getFiatStakingBalance(blockchainId: String): BigDecimal { - val yieldBalance = yieldBalance as? YieldBalance.Data - val stakingBalance = yieldBalance?.getTotalWithRewardsStakingBalance(blockchainId).orZero() + val stakingBalanceData = stakingBalance as? StakingBalance.Data + val totalStakingBalance = stakingBalanceData?.getTotalWithRewardsStakingBalance(blockchainId).orZero() - return fiatRate.times(stakingBalance) + return fiatRate.times(totalStakingBalance) } private fun CryptoCurrencyStatus.Custom.getFiatStakingBalance(blockchainId: String): BigDecimal { - val yieldBalance = yieldBalance as? YieldBalance.Data - val stakingBalance = yieldBalance?.getTotalWithRewardsStakingBalance(blockchainId).orZero() + val stakingBalanceData = stakingBalance as? StakingBalance.Data + val totalStakingBalance = stakingBalanceData?.getTotalWithRewardsStakingBalance(blockchainId).orZero() - return fiatRate?.times(stakingBalance).orZero() + return fiatRate?.times(totalStakingBalance).orZero() } private inline fun TotalFiatBalance.fold( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt index b917edebde..59937cd45f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt @@ -11,8 +11,8 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.network.getAddress import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error @@ -28,20 +28,20 @@ class CurrencyStatusProxyCreator { currency: CryptoCurrency, maybeQuoteStatus: Either, maybeNetworkStatus: Either, - maybeYieldBalance: Either?, + maybeStakingBalance: Either?, ): Either = either { val networkStatus = maybeNetworkStatus.bind() val quote = recover( block = { maybeQuoteStatus.bind() }, recover = { null }, ) - val yieldBalance = maybeYieldBalance?.getOrNull() + val stakingBalance = maybeStakingBalance?.getOrNull() createCurrencyStatus( currency = currency, quoteStatus = quote, networkStatus = networkStatus, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, ) } @@ -49,12 +49,12 @@ class CurrencyStatusProxyCreator { currencies: NonEmptyList, maybeQuotes: Either>?, maybeNetworkStatuses: Either>, - maybeYieldBalances: Either>, + maybeStakingBalances: Either>, ): Either> = either { val networksStatuses = maybeNetworkStatuses.bind().toNonEmptySetOrNull() val quoteStatuses: Set? = maybeQuotes?.getOrNull()?.ifEmpty { null } - val yieldBalances = maybeYieldBalances.getOrNull() + val stakingBalances = maybeStakingBalances.getOrNull() currencies.map { currency -> val quote = quoteStatuses?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } @@ -63,11 +63,11 @@ class CurrencyStatusProxyCreator { val supportedIntegration = StakingIntegrationID.create(currencyId = currency.id)?.value - val yieldBalance = if (supportedIntegration != null && address != null) { + val stakingBalance = if (supportedIntegration != null && address != null) { val stakingId = StakingID(integrationId = supportedIntegration, address = address) - yieldBalances?.firstOrNull { it.stakingId == stakingId } - ?: YieldBalance.Error(stakingId = stakingId) + stakingBalances?.firstOrNull { it.stakingId == stakingId } + ?: StakingBalance.Error(stakingId = stakingId) } else { null } @@ -76,7 +76,7 @@ class CurrencyStatusProxyCreator { currency = currency, quoteStatus = quote, networkStatus = networkStatus, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, ) } } @@ -85,13 +85,13 @@ class CurrencyStatusProxyCreator { currency: CryptoCurrency, quoteStatus: QuoteStatus?, networkStatus: NetworkStatus?, - yieldBalance: YieldBalance?, + stakingBalance: StakingBalance?, ): CryptoCurrencyStatus { return CryptoCurrencyStatusFactory.create( currency = currency, maybeNetworkStatus = networkStatus.toOption(), maybeQuoteStatus = quoteStatus.toOption(), - maybeYieldBalance = yieldBalance.toOption(), + maybeStakingBalance = stakingBalance.toOption(), ) } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt index bceb16c296..06b3f5f4ee 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt @@ -9,7 +9,7 @@ import com.tangem.domain.models.wallet.UserWalletId 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.MultiWalletCryptoCurrenciesFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -31,7 +31,7 @@ import timber.log.Timber * @property singleWalletBalanceFetcher balance fetcher of single-currency wallet * @property multiNetworkStatusFetcher networks statuses fetcher * @property multiQuoteStatusFetcher quotes statuses fetcher - * @property multiYieldBalanceFetcher yields balances fetcher + * @property multiStakingBalanceFetcher yields balances fetcher * @property dispatchers dispatchers * [REDACTED_AUTHOR] @@ -44,7 +44,7 @@ class WalletBalanceFetcher internal constructor( private val singleWalletBalanceFetcher: BaseWalletBalanceFetcher, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, private val stakingIdFactory: StakingIdFactory, private val dispatchers: CoroutineDispatcherProvider, ) : FlowFetcher { @@ -56,7 +56,7 @@ class WalletBalanceFetcher internal constructor( multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + multiStakingBalanceFetcher: MultiStakingBalanceFetcher, stakingIdFactory: StakingIdFactory, dispatchers: CoroutineDispatcherProvider, ) : this( @@ -71,7 +71,7 @@ class WalletBalanceFetcher internal constructor( singleWalletBalanceFetcher = SingleWalletBalanceFetcher(currenciesRepository = currenciesRepository), multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, - multiYieldBalanceFetcher = multiYieldBalanceFetcher, + multiStakingBalanceFetcher = multiStakingBalanceFetcher, stakingIdFactory = stakingIdFactory, dispatchers = dispatchers, ) @@ -164,8 +164,8 @@ class WalletBalanceFetcher internal constructor( val stakingIds = maybeStakingIds.mapNotNullTo(hashSetOf()) { it.getOrNull() } if (stakingIds.isNotEmpty()) { - multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), + multiStakingBalanceFetcher( + params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), ) .bind() } else { diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index a9b9b31d66..1121756a55 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -154,7 +154,7 @@ internal object MockTokensStates { networkAddress = requireNotNull( value = MockNetworks.verifiedNetworksStatuses.first { it.network == status.currency.network }.value as? NetworkStatus.Verified, ).address, - yieldBalance = null, + stakingBalance = null, sources = CryptoCurrencyStatus.Sources(), yieldSupplyStatus = null, ) @@ -166,7 +166,7 @@ internal object MockTokensStates { pendingTransactions = emptySet(), hasCurrentNetworkTransactions = false, networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address, - yieldBalance = null, + stakingBalance = null, sources = CryptoCurrencyStatus.Sources(), yieldSupplyStatus = null, ) @@ -185,7 +185,7 @@ internal object MockTokensStates { .first { it.network == status.currency.network } .value as? NetworkStatus.Verified, ).address, - yieldBalance = null, + stakingBalance = null, sources = CryptoCurrencyStatus.Sources(), yieldSupplyStatus = null, ), diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactoryTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactoryTest.kt index 22c3bb6eff..84fa8cedeb 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactoryTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactoryTest.kt @@ -14,8 +14,8 @@ import com.tangem.domain.models.network.NetworkStatus.Amount import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.quote.QuoteStatus 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.YieldBalance import com.tangem.domain.models.staking.YieldBalanceItem import com.tangem.domain.models.yield.supply.YieldSupplyStatus import com.tangem.domain.staking.model.StakingIntegrationID @@ -54,7 +54,7 @@ class CryptoCurrencyStatusFactoryTest { inner class MissedDerivation { private val networkStatus = NetworkStatus.MissedDerivation.toStatus() - private val maybeYieldBalance = none() // not relevant for this test + private val maybeStakingBalance = none() // not relevant for this test @Test fun `network is MissedDerivation and QuoteStatus is Data`() { @@ -63,7 +63,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = fullQuote.toStatus().some(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -82,7 +82,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = emptyQuoteStatus.some(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -97,7 +97,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = none(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -112,7 +112,7 @@ class CryptoCurrencyStatusFactoryTest { private val networkStatus = NetworkStatus.Unreachable(address = networkAddress).toStatus() - private val maybeYieldBalance = none() // not relevant for this test + private val maybeStakingBalance = none() // not relevant for this test @Test fun `network is Unreachable and QuoteStatus is Data`() { @@ -121,7 +121,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = fullQuote.toStatus().some(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -141,7 +141,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = emptyQuoteStatus.some(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -161,7 +161,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = none(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -185,7 +185,7 @@ class CryptoCurrencyStatusFactoryTest { source = StatusSource.ACTUAL, ).toStatus() - private val maybeYieldBalance = none() // not relevant for this test + private val maybeStakingBalance = none() // not relevant for this test @Test fun `network is NoAccount and QuoteStatus is Data`() { @@ -194,7 +194,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = fullQuote.toStatus().some(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -217,7 +217,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = emptyQuoteStatus.some(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -239,7 +239,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = none(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -261,7 +261,7 @@ class CryptoCurrencyStatusFactoryTest { private val networkStatus = createVerified(amounts = mapOf(currency.id to Amount.NotFound)).toStatus() - private val maybeYieldBalance = none() // not relevant for this test + private val maybeStakingBalance = none() // not relevant for this test @Test fun `network is Verified with Amount is NotFound and QuoteStatus is Data`() { @@ -270,7 +270,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = fullQuote.toStatus().some(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -289,7 +289,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = emptyQuoteStatus.some(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -304,7 +304,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = none(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -317,7 +317,7 @@ class CryptoCurrencyStatusFactoryTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class Loading { - private val maybeYieldBalance = none() // not relevant for this test + private val maybeStakingBalance = none() // not relevant for this test @Test fun `network is null`() { @@ -326,7 +326,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = none(), maybeQuoteStatus = none(), // not relevant for this test, - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -344,7 +344,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = none(), // not relevant for this test, - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -364,7 +364,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = none(), - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -387,14 +387,14 @@ class CryptoCurrencyStatusFactoryTest { fun `network is Verified, QuoteStatus is null, YieldBalance is null`() { // Arrange val maybeQuoteStatus = none() - val maybeYieldBalance = none() + val maybeStakingBalance = none() // Act val actual = CryptoCurrencyStatusFactory.create( currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = maybeQuoteStatus, - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -405,7 +405,7 @@ class CryptoCurrencyStatusFactoryTest { fiatAmount = null, fiatRate = null, priceChange = null, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -420,7 +420,7 @@ class CryptoCurrencyStatusFactoryTest { @Test fun `network is Verified, QuoteStatus is Data, YieldBalance is Data`() { // Arrange - val yieldBalance = YieldBalance.Data( + val stakeKitBalance = StakingBalance.Data.StakeKit( stakingId = StakingID( integrationId = StakingIntegrationID.StakeKit.Coin.Cardano.value, address = networkAddress.defaultAddress.value, @@ -444,7 +444,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = fullQuote.toStatus().some(), - maybeYieldBalance = yieldBalance.some(), + maybeStakingBalance = stakeKitBalance.some(), ) // Assert @@ -455,9 +455,9 @@ class CryptoCurrencyStatusFactoryTest { fiatAmount = BigDecimal.TEN * fullQuote.fiatRate, fiatRate = fullQuote.fiatRate, priceChange = fullQuote.priceChange, - yieldBalance = yieldBalance.copy( - balance = yieldBalance.balance.copy( - items = yieldBalance.balance.items.subList(0, 1), + stakingBalance = stakeKitBalance.copy( + balance = stakeKitBalance.balance.copy( + items = stakeKitBalance.balance.items.subList(0, 1), ), ), yieldSupplyStatus = null, @@ -490,20 +490,20 @@ class CryptoCurrencyStatusFactoryTest { @Test fun `network is Verified and YieldBalance is null`() { // Arrange - val maybeYieldBalance = none() + val maybeStakingBalance = none() // Act val actual = CryptoCurrencyStatusFactory.create( currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = maybeQuoteStatus, - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert val expected = CryptoCurrencyStatus.NoQuote( amount = BigDecimal.TEN, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = yieldSupplyStatuses[currency.id]!!, hasCurrentNetworkTransactions = true, pendingTransactions = pendingTransactions[currency.id]!!, @@ -517,7 +517,7 @@ class CryptoCurrencyStatusFactoryTest { @Test fun `network is Verified and YieldBalance is Data`() { // Arrange - val yieldBalance = YieldBalance.Data( + val stakeKitBalance = StakingBalance.Data.StakeKit( stakingId = StakingID( integrationId = StakingIntegrationID.StakeKit.Coin.Cardano.value, address = networkAddress.defaultAddress.value, @@ -541,15 +541,15 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = maybeQuoteStatus, - maybeYieldBalance = yieldBalance.some(), + maybeStakingBalance = stakeKitBalance.some(), ) // Assert val expected = CryptoCurrencyStatus.NoQuote( amount = BigDecimal.TEN, - yieldBalance = yieldBalance.copy( - balance = yieldBalance.balance.copy( - items = yieldBalance.balance.items.subList(0, 1), + stakingBalance = stakeKitBalance.copy( + balance = stakeKitBalance.balance.copy( + items = stakeKitBalance.balance.items.subList(0, 1), ), ), yieldSupplyStatus = yieldSupplyStatuses[currency.id]!!, @@ -576,14 +576,14 @@ class CryptoCurrencyStatusFactoryTest { @Test fun `network is Verified and YieldBalance is null`() { // Arrange - val maybeYieldBalance = none() + val maybeStakingBalance = none() // Act val actual = CryptoCurrencyStatusFactory.create( currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = maybeQuoteStatus, - maybeYieldBalance = maybeYieldBalance, + maybeStakingBalance = maybeStakingBalance, ) // Assert @@ -592,7 +592,7 @@ class CryptoCurrencyStatusFactoryTest { fiatAmount = BigDecimal.TEN * fullQuote.fiatRate, fiatRate = fullQuote.fiatRate, priceChange = fullQuote.priceChange, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -606,7 +606,7 @@ class CryptoCurrencyStatusFactoryTest { @Test fun `network is Verified and YieldBalance is Data`() { // Arrange - val yieldBalance = YieldBalance.Data( + val stakeKitBalance = StakingBalance.Data.StakeKit( stakingId = StakingID( integrationId = StakingIntegrationID.StakeKit.Coin.Cardano.value, address = networkAddress.defaultAddress.value, @@ -630,7 +630,7 @@ class CryptoCurrencyStatusFactoryTest { currency = currency, maybeNetworkStatus = networkStatus.some(), maybeQuoteStatus = fullQuote.toStatus().some(), - maybeYieldBalance = yieldBalance.some(), + maybeStakingBalance = stakeKitBalance.some(), ) // Assert @@ -639,9 +639,9 @@ class CryptoCurrencyStatusFactoryTest { fiatAmount = BigDecimal.TEN * fullQuote.fiatRate, fiatRate = fullQuote.fiatRate, priceChange = fullQuote.priceChange, - yieldBalance = yieldBalance.copy( - balance = yieldBalance.balance.copy( - items = yieldBalance.balance.items.subList(0, 1), + stakingBalance = stakeKitBalance.copy( + balance = stakeKitBalance.balance.copy( + items = stakeKitBalance.balance.items.subList(0, 1), ), ), yieldSupplyStatus = null, diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/PriceChangeCalculatorTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/PriceChangeCalculatorTest.kt index 7dbbeec80b..93087cadfc 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/PriceChangeCalculatorTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/PriceChangeCalculatorTest.kt @@ -137,7 +137,7 @@ class PriceChangeCalculatorTest { fiatAmount = amount, fiatRate = BigDecimal.ONE, priceChange = priceChange, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculatorTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculatorTest.kt index 5c2a253292..7f08ae5dbc 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculatorTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculatorTest.kt @@ -11,7 +11,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.staking.BalanceItem import com.tangem.domain.models.staking.BalanceType -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.YieldBalanceItem import io.mockk.every import io.mockk.mockk @@ -192,7 +192,7 @@ class TotalFiatBalanceCalculatorTest { createCustom( currency = cryptoCurrencyFactory.cardano, fiatAmount = BigDecimal.ZERO, - yieldBalance = createYieldBalance( + stakingBalance = createStakeKitBalance( amount = BigDecimal(20), // It is important to use `BalanceType.REWARDS` because Cardano should not include the full // staking balance. See `getTotalWithRewardsStakingBalance`. @@ -207,7 +207,7 @@ class TotalFiatBalanceCalculatorTest { createCustom( currency = cryptoCurrencyFactory.stellar, fiatAmount = BigDecimal.ONE, - yieldBalance = createYieldBalance( + stakingBalance = createStakeKitBalance( amount = BigDecimal(9), balanceType = BalanceType.STAKED, ), @@ -246,7 +246,7 @@ class TotalFiatBalanceCalculatorTest { createLoaded( currency = cryptoCurrencyFactory.cardano, fiatAmount = BigDecimal.ZERO, - yieldBalance = createYieldBalance( + stakingBalance = createStakeKitBalance( amount = BigDecimal(20), // It is important to use `BalanceType.REWARDS` because Cardano should not include the full // staking balance. See `getTotalWithRewardsStakingBalance`. @@ -261,7 +261,7 @@ class TotalFiatBalanceCalculatorTest { createLoaded( currency = cryptoCurrencyFactory.stellar, fiatAmount = BigDecimal.ONE, - yieldBalance = createYieldBalance( + stakingBalance = createStakeKitBalance( amount = BigDecimal(9), balanceType = BalanceType.STAKED, ), @@ -497,7 +497,7 @@ class TotalFiatBalanceCalculatorTest { currency = currency, value = CryptoCurrencyStatus.NoQuote( amount = BigDecimal.ONE, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -545,7 +545,7 @@ class TotalFiatBalanceCalculatorTest { private fun createCustom( currency: CryptoCurrency, fiatAmount: BigDecimal?, - yieldBalance: YieldBalance? = null, + stakingBalance: StakingBalance.Data.StakeKit? = null, ): CryptoCurrencyStatus { return CryptoCurrencyStatus( currency = currency, @@ -554,7 +554,7 @@ class TotalFiatBalanceCalculatorTest { fiatAmount = fiatAmount, fiatRate = BigDecimal.ONE, priceChange = BigDecimal.ZERO, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -567,7 +567,7 @@ class TotalFiatBalanceCalculatorTest { private fun createLoaded( currency: CryptoCurrency, fiatAmount: BigDecimal, - yieldBalance: YieldBalance? = null, + stakingBalance: StakingBalance.Data.StakeKit? = null, source: StatusSource = StatusSource.ACTUAL, ): CryptoCurrencyStatus { return CryptoCurrencyStatus( @@ -577,7 +577,7 @@ class TotalFiatBalanceCalculatorTest { fiatAmount = fiatAmount, fiatRate = BigDecimal.ONE, priceChange = BigDecimal.ZERO, - yieldBalance = yieldBalance, + stakingBalance = stakingBalance, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -587,8 +587,8 @@ class TotalFiatBalanceCalculatorTest { ) } - private fun createYieldBalance(amount: BigDecimal, balanceType: BalanceType): YieldBalance.Data { - return YieldBalance.Data( + private fun createStakeKitBalance(amount: BigDecimal, balanceType: BalanceType): StakingBalance.Data.StakeKit { + return StakingBalance.Data.StakeKit( stakingId = mockk(), source = StatusSource.ACTUAL, balance = YieldBalanceItem( diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt index f2eb68dbdd..2af6690bc8 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt @@ -12,7 +12,7 @@ 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.model.StakingIntegrationID -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.wallet.FetchingSource.* import com.tangem.domain.tokens.wallet.implementor.MultiWalletBalanceFetcher @@ -41,7 +41,7 @@ internal class WalletBalanceFetcherTest { private val singleWalletBalanceFetcher: SingleWalletBalanceFetcher = mockk() private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher = mockk() private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher = mockk() - private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher = mockk() + private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher = mockk() private val stakingIdFactory: StakingIdFactory = mockk() private val fetcher = WalletBalanceFetcher( @@ -51,7 +51,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher = singleWalletBalanceFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, - multiYieldBalanceFetcher = multiYieldBalanceFetcher, + multiStakingBalanceFetcher = multiStakingBalanceFetcher, stakingIdFactory = stakingIdFactory, dispatchers = TestingCoroutineDispatcherProvider(), ) @@ -65,7 +65,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher, multiNetworkStatusFetcher, multiQuoteStatusFetcher, - multiYieldBalanceFetcher, + multiStakingBalanceFetcher, ) } @@ -91,7 +91,7 @@ internal class WalletBalanceFetcherTest { multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -122,7 +122,7 @@ internal class WalletBalanceFetcherTest { multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -156,7 +156,7 @@ internal class WalletBalanceFetcherTest { multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -188,7 +188,7 @@ internal class WalletBalanceFetcherTest { multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -234,7 +234,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiQuoteStatusFetcher(params = any()) stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -280,7 +280,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiNetworkStatusFetcher(params = any()) stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -292,7 +292,7 @@ internal class WalletBalanceFetcherTest { } val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() - val yieldBalanceFetcherParams = MultiYieldBalanceFetcher.Params( + val stakingBalanceFetcherParams = MultiStakingBalanceFetcher.Params( userWalletId = userWalletId, stakingIds = setOf(ethereumStakingId, stellarStakingId), ) @@ -308,7 +308,7 @@ internal class WalletBalanceFetcherTest { coEvery { stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) } returns Either.Right(stellarStakingId) - coEvery { multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } returns exception.left() + coEvery { multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } returns exception.left() // Act val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) @@ -326,7 +326,7 @@ internal class WalletBalanceFetcherTest { multiWalletBalanceFetcher.fetchingSources stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) - multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) + multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } coVerify(inverse = true) { @@ -372,7 +372,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -414,7 +414,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -462,7 +462,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -485,7 +485,7 @@ internal class WalletBalanceFetcherTest { appCurrencyId = null, ) - val yieldBalanceFetcherParams = MultiYieldBalanceFetcher.Params( + val stakingBalanceFetcherParams = MultiStakingBalanceFetcher.Params( userWalletId = userWalletId, stakingIds = setOf(ethereumStakingId, stellarStakingId), ) @@ -503,7 +503,7 @@ internal class WalletBalanceFetcherTest { coEvery { stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) } returns Either.Right(stellarStakingId) - coEvery { multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } returns exception.left() + coEvery { multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } returns exception.left() // Act val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) @@ -525,7 +525,7 @@ internal class WalletBalanceFetcherTest { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) - multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) + multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } coVerify(inverse = true) { @@ -553,7 +553,7 @@ internal class WalletBalanceFetcherTest { appCurrencyId = null, ) - val yieldBalanceFetcherParams = MultiYieldBalanceFetcher.Params( + val stakingBalanceFetcherParams = MultiStakingBalanceFetcher.Params( userWalletId = userWalletId, stakingIds = setOf(ethereumStakingId, stellarStakingId), ) @@ -569,7 +569,7 @@ internal class WalletBalanceFetcherTest { coEvery { stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) } returns Either.Right(stellarStakingId) - coEvery { multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } returns Unit.right() + coEvery { multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } returns Unit.right() // Act val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) @@ -586,7 +586,7 @@ internal class WalletBalanceFetcherTest { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) - multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) + multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } coVerify(inverse = true) { @@ -642,7 +642,7 @@ internal class WalletBalanceFetcherTest { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } @@ -692,7 +692,7 @@ internal class WalletBalanceFetcherTest { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) - multiYieldBalanceFetcher(params = any()) + multiStakingBalanceFetcher(params = any()) } } diff --git a/domain/transaction/models/detekt-baseline-main.xml b/domain/transaction/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..c826f4a41a --- /dev/null +++ b/domain/transaction/models/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + NullableToStringCall:SendTransactionError.kt$SendTransactionError$$code + + diff --git a/domain/txhistory/models/detekt-baseline-main.xml b/domain/txhistory/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..c1da5a5bd3 --- /dev/null +++ b/domain/txhistory/models/detekt-baseline-main.xml @@ -0,0 +1,8 @@ + + + + + ObjectExtendsThrowable:TxHistoryStateError.kt$TxHistoryStateError$EmptyTxHistories : TxHistoryStateError + ObjectExtendsThrowable:TxHistoryStateError.kt$TxHistoryStateError$TxHistoryNotImplemented : TxHistoryStateError + + diff --git a/domain/visa/models/detekt-baseline-main.xml b/domain/visa/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..674a5f7aed --- /dev/null +++ b/domain/visa/models/detekt-baseline-main.xml @@ -0,0 +1,14 @@ + + + + + NamedArguments:VisaCardActivationStatus.kt$VisaCardActivationStatus_JsonAdapter$ActivationStarted( value.activationInput!!, value.authTokens!!, value.remoteState!!, value.cardWalletAddress!!, ) + NamedArguments:VisaCardActivationStatus.kt$VisaCardActivationStatus_JsonAdapter$VisaCardActivationStatus_Json( VisaCardActivationStatus_Type.ActivationStarted, value.activationInput, value.authTokens, value.remoteState, value.cardWalletAddress, ) + UnsafeCallOnNullableType:VisaActivationRemoteState.kt$VisaActivationRemoteState_JsonAdapter$value.activationOrderInfo!! + UnsafeCallOnNullableType:VisaActivationRemoteState.kt$VisaActivationRemoteState_JsonAdapter$value.awaitingPinCodeStatus!! + UnsafeCallOnNullableType:VisaCardActivationStatus.kt$VisaCardActivationStatus_JsonAdapter$value.activationInput!! + UnsafeCallOnNullableType:VisaCardActivationStatus.kt$VisaCardActivationStatus_JsonAdapter$value.authTokens!! + UnsafeCallOnNullableType:VisaCardActivationStatus.kt$VisaCardActivationStatus_JsonAdapter$value.cardWalletAddress!! + UnsafeCallOnNullableType:VisaCardActivationStatus.kt$VisaCardActivationStatus_JsonAdapter$value.remoteState!! + + diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt index 82b9328d08..2f944349f5 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt @@ -38,16 +38,16 @@ sealed class TangemPayAnalyticsEvents( event = "Visa Main Screen Opened", ) - class AddFundsClicked : TangemPayAnalyticsEvents( - categoryName = "Visa Screen", - event = "Button - Visa Add Funds", - ) - class ReceiveFundsClicked : TangemPayAnalyticsEvents( categoryName = "Visa Screen", event = "Button - Visa Receive", ) + class AddFundsClicked : TangemPayAnalyticsEvents( + categoryName = "Visa Screen", + event = "Button - Visa Add Funds", + ) + class SwapClicked : TangemPayAnalyticsEvents( categoryName = "Visa Screen", event = "Button - Visa Swap", diff --git a/domain/wallet-connect/models/detekt-baseline-main.xml b/domain/wallet-connect/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..a019799a9b --- /dev/null +++ b/domain/wallet-connect/models/detekt-baseline-main.xml @@ -0,0 +1,12 @@ + + + + + BooleanPropertyNaming:WcAppMetaData.kt$WcAppMetaData$val linkMode: Boolean = false + BooleanPropertyNaming:WcSession.kt$WcSession$val showWalletInfo: Boolean + ObjectExtendsThrowable:WcPairError.kt$WcPairError$InvalidConnectionRequest : WcPairError + ObjectExtendsThrowable:WcPairError.kt$WcPairError$InvalidDomainURL : WcPairError + ObjectExtendsThrowable:WcPairError.kt$WcPairError$ProposalExpired : WcPairError + ObjectExtendsThrowable:WcPairError.kt$WcPairError$RejectionFailed : WcPairError + + diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt index 8bcf3eb116..aba74660de 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt @@ -5,7 +5,7 @@ import com.domain.blockaid.models.dapp.CheckDAppResult.* import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.models.network.Network -import com.tangem.domain.walletconnect.WcAnalyticEvents.ButtonDisconnectAll.toAnalyticVerificationStatus +import com.tangem.domain.walletconnect.WcAnalyticEvents.DAppVerificationStatus import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionApprove @@ -19,7 +19,7 @@ sealed class WcAnalyticEvents( params: Map = emptyMap(), ) : AnalyticsEvent(category = WC_CATEGORY_NAME, event = event, params = params) { - object ScreenOpened : WcAnalyticEvents(event = "WC Screen Opened") + class ScreenOpened : WcAnalyticEvents(event = "WC Screen Opened") class NewPairInitiated(source: WcPairRequest.Source) : WcAnalyticEvents( event = "Session Initiated", params = mapOf( @@ -32,7 +32,7 @@ sealed class WcAnalyticEvents( ), ) - data object PairButtonConnect : WcAnalyticEvents( + class PairButtonConnect : WcAnalyticEvents( event = "Button - Connect", ) @@ -203,7 +203,7 @@ sealed class WcAnalyticEvents( } } - data object ButtonDisconnectAll : WcAnalyticEvents( + class ButtonDisconnectAll : WcAnalyticEvents( event = "Button - Disconnect All", ) @@ -265,16 +265,35 @@ sealed class WcAnalyticEvents( Unknown("Unknown"), } - fun CheckDAppResult.toAnalyticVerificationStatus(): String = when (this) { - SAFE -> DAppVerificationStatus.Verified - UNSAFE -> DAppVerificationStatus.Risky - FAILED_TO_VERIFY -> DAppVerificationStatus.Unknown - }.status - companion object { const val NETWORKS = "Networks" const val DOMAIN_VERIFICATION = "Domain Verification" const val WC_CATEGORY_NAME = "Wallet Connect" } +} + +fun CheckDAppResult.toAnalyticVerificationStatus(): String = when (this) { + SAFE -> DAppVerificationStatus.Verified + UNSAFE -> DAppVerificationStatus.Risky + FAILED_TO_VERIFY -> DAppVerificationStatus.Unknown +}.status + +sealed class WcAnalyticAccountEvents( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(category = WC_CATEGORY_ACCOUNT_NAME, event = event, params = params) { + + data class PairButtonConnect( + private val accountDerivation: Int, + ) : WcAnalyticAccountEvents( + event = "Button - Connect", + params = mapOf( + "Account Derivation" to accountDerivation.toString(), + ), + ) + + companion object { + const val WC_CATEGORY_ACCOUNT_NAME = "WalletConnect - Account" + } } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt index cb6c3a2497..8c73940afe 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt @@ -15,7 +15,7 @@ class WcDisconnectUseCase( ) { suspend fun disconnectAll() { - analytics.send(WcAnalyticEvents.ButtonDisconnectAll) + analytics.send(WcAnalyticEvents.ButtonDisconnectAll()) sessionsManager.sessions.first() .flatMap { it.value } .map { session -> flow { emit(internalDisconnect(session)) } } diff --git a/domain/wallet-manager/models/detekt-baseline-main.xml b/domain/wallet-manager/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..ecf2e0cce8 --- /dev/null +++ b/domain/wallet-manager/models/detekt-baseline-main.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index ad9162e60a..a5779a9b3f 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.notifications.models) implementation(projects.domain.demo.models) + implementation(projects.domain.hotWallet) // endregion // region Tangem libraries diff --git a/domain/wallets/detekt-baseline-debug.xml b/domain/wallets/detekt-baseline-debug.xml index 149134a54c..b55688a97f 100644 --- a/domain/wallets/detekt-baseline-debug.xml +++ b/domain/wallets/detekt-baseline-debug.xml @@ -28,8 +28,6 @@ MultilineLambdaItParameter:UpdateWalletUseCase.kt$UpdateWalletUseCase${ when (it) { is SaveWalletError.DataError -> DataError( IllegalStateException("Failed to update wallet: ${it.messageId}"), ) is SaveWalletError.WalletAlreadySaved -> UpdateWalletError.NameAlreadyExists } } NestedScopeFunctions:ColdUserWalletBuilder.kt$ColdUserWalletBuilder$let { UserWallet.Cold( walletId = it, name = generateWalletNameUseCase( card = card, productType = productType, isStartToCoin = cardTypesResolver.isStart2Coin(), ), cardsInWallet = backupCardsIds.plus(card.cardId), scanResponse = this, isMultiCurrency = cardTypesResolver.isMultiwalletAllowed(), hasBackupError = hasBackupError, ) } NoNameShadowing:SaveWalletUseCase.kt$SaveWalletUseCase$userWallet - NonBooleanPropertyPrefixedWithIs:GetExtendedPublicKeyForCurrencyUseCase.kt$GetExtendedPublicKeyForCurrencyUseCase$val isHdKey = walletManager.wallet.publicKey.derivationType?.hdKey - NonBooleanPropertyPrefixedWithIs:UserWalletsListManagerExtensions.kt$/** * Indicates that the [UserWalletsListManager] is locked * * @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] returns [Flow] which * produces only one false value * * @see UserWalletsListManager.Lockable.isLocked * */ val UserWalletsListManager.isLocked: Flow<Boolean> get() = asLockable()?.lockedState ?: flowOf(false) NullableToStringCall:GenerateBuyTangemCardLinkUseCase.kt$GenerateBuyTangemCardLinkUseCase$$id NullableToStringCall:UpdateWalletUseCase.kt$UpdateWalletUseCase$${it.messageId} ObjectExtendsThrowable:UserWalletsListError.kt$UserWalletsListError$AllKeysInvalidated : UserWalletsListError diff --git a/domain/wallets/models/detekt-baseline-main.xml b/domain/wallets/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..0b3473e462 --- /dev/null +++ b/domain/wallets/models/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + ObjectExtendsThrowable:ParsedQrCodeErrors.kt$ParsedQrCodeErrors$InvalidUriError : ParsedQrCodeErrors + + diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt index 6bc640e1e2..703dcb8cbb 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt @@ -8,7 +8,5 @@ sealed class Settings( params: Map = emptyMap(), ) : AnalyticsEvent(category, event, params) { - data object ButtonCreateBackup : Settings(event = "Button - Create Backup") - - data object ButtonManageTokens : Settings(event = "Button - Manage Tokens") + class ButtonManageTokens : Settings(event = "Button - Manage Tokens") } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt index 4a46695ee3..1c23c85475 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt @@ -16,14 +16,35 @@ sealed class WalletSettingsAnalyticEvents( params = mapOf(STATUS to enabled.value), ) - object WalletSettingsScreenOpened : WalletSettingsAnalyticEvents( + class WalletSettingsScreenOpened( + private val accountsCount: Int?, + ) : WalletSettingsAnalyticEvents( event = "Wallet Settings Screen Opened", + params = buildMap { + if (accountsCount != null) put("Accounts Count", accountsCount.toString()) + }, ) - object ButtonBackup : WalletSettingsAnalyticEvents( + class ButtonBackup : WalletSettingsAnalyticEvents( event = "Button - Backup", ) + class ButtonAddAccount : WalletSettingsAnalyticEvents( + event = "Button - Add Account", + ) + + class ButtonOpenExistingAccount : WalletSettingsAnalyticEvents( + event = "Button - Open Existing Account", + ) + + class ButtonArchivedAccounts : WalletSettingsAnalyticEvents( + event = "Button - Archived Accounts", + ) + + class LongtapAccountsOrder : WalletSettingsAnalyticEvents( + event = "Longtap - Accounts Order", + ) + data class ButtonAccessCode( private val isCodeSet: Boolean, ) : WalletSettingsAnalyticEvents( @@ -32,13 +53,13 @@ sealed class WalletSettingsAnalyticEvents( ) data class BackupScreenOpened( - val isManualBackupEnabled: Boolean, + val isBackedUp: Boolean, ) : WalletSettingsAnalyticEvents( event = "Backup Screen Opened", - params = mapOf("Manual Backup" to if (isManualBackupEnabled) "Enabled" else "Disabled"), + params = mapOf("Manual Backup" to if (isBackedUp) "Yes" else "No"), ) - object ButtonRecoveryPhrase : WalletSettingsAnalyticEvents( + class ButtonRecoveryPhrase : WalletSettingsAnalyticEvents( event = "Button - Recovery phrase", ) @@ -59,27 +80,27 @@ sealed class WalletSettingsAnalyticEvents( } } - object ButtonHardwareUpdate : WalletSettingsAnalyticEvents( + class ButtonHardwareUpdate : WalletSettingsAnalyticEvents( event = "Button - Hardware Update", ) - object HardwareUpgradeScreenOpened : WalletSettingsAnalyticEvents( + class HardwareUpgradeScreenOpened : WalletSettingsAnalyticEvents( event = "Hardware Upgrade Screen Opened", ) - object ButtonCreateNewWallet : WalletSettingsAnalyticEvents( + class ButtonCreateNewWallet : WalletSettingsAnalyticEvents( event = "Button - Create New Wallet", ) - object ButtonUpgradeCurrent : WalletSettingsAnalyticEvents( + class ButtonUpgradeCurrent : WalletSettingsAnalyticEvents( event = "Button - Upgrade Current", ) - object CreateWalletScreenOpened : WalletSettingsAnalyticEvents( + class CreateWalletScreenOpened : WalletSettingsAnalyticEvents( event = "Create Wallet Screen Opened", ) - object HardwareBackupScreenOpened : WalletSettingsAnalyticEvents( + class HardwareBackupScreenOpened : WalletSettingsAnalyticEvents( event = "Hardware Backup Screen Opened", ) @@ -141,7 +162,7 @@ sealed class WalletSettingsAnalyticEvents( params = mapOf(AnalyticsParam.Key.SOURCE to source), ) - object ButtonStartUpgrade : WalletSettingsAnalyticEvents( + class ButtonStartUpgrade : WalletSettingsAnalyticEvents( event = "Button - Start Upgrade", ) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt index eaa1c7dc7c..de81ddc111 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt @@ -3,6 +3,7 @@ package com.tangem.domain.wallets.builder import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.domain.hotwallet.IsHotWalletCreationSupported import com.tangem.domain.models.MobileWallet import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase @@ -22,9 +23,12 @@ class HotUserWalletBuilder @AssistedInject constructor( private val hotSdk: TangemHotSdk, private val generateWalletNameUseCase: GenerateWalletNameUseCase, private val dispatcherProvider: CoroutineDispatcherProvider, + private val isHotWalletCreationSupported: IsHotWalletCreationSupported, ) { suspend fun build(): UserWallet.Hot = withContext(dispatcherProvider.default) { + checkHotWalletCreationSupported() + val allNetworks = Blockchain.entries.filter { it.isTestnet().not() } val curves = allNetworks.map { it.getSupportedCurves() }.flatten().toSet() val requests = curves.sortedBy { it.ordinal }.map { curve -> @@ -73,6 +77,12 @@ class HotUserWalletBuilder @AssistedInject constructor( ) } + fun checkHotWalletCreationSupported() { + require(isHotWalletCreationSupported()) { + "Hot wallet creation is supported only from ${isHotWalletCreationSupported.getLeastVersionName()}" + } + } + @AssistedFactory interface Factory { fun create(hotWalletId: HotWalletId): HotUserWalletBuilder diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 2cbdf16c5c..96266b809d 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -64,10 +64,13 @@ interface WalletsRepository { suspend fun dismissUpgradeWalletNotification(userWalletId: UserWalletId) @Throws - suspend fun setWalletName(walletId: String, walletName: String) + suspend fun setWalletName(walletId: UserWalletId, walletName: String) @Throws - suspend fun getWalletInfo(walletId: String): UserWalletRemoteInfo + suspend fun upgradeWallet(walletId: UserWalletId) + + @Throws + suspend fun getWalletInfo(walletId: UserWalletId): UserWalletRemoteInfo @Throws suspend fun getWalletsInfo(applicationId: String, updateCache: Boolean = true): List diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt index b69d760d71..2f5ffa9dde 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt @@ -16,7 +16,7 @@ class RenameWalletUseCase( suspend operator fun invoke(userWalletId: UserWalletId, name: String): Either = either { runCatching { - walletsRepository.setWalletName(userWalletId.stringValue, name) + walletsRepository.setWalletName(userWalletId, name) } userWalletsSyncDelegate.syncWallet(userWalletId, name).bind() diff --git a/domain/yield-supply/models/detekt-baseline-main.xml b/domain/yield-supply/models/detekt-baseline-main.xml new file mode 100644 index 0000000000..9407c35741 --- /dev/null +++ b/domain/yield-supply/models/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + NullableToStringCall:YieldMarketToken.kt$YieldMarketToken$${backendId} + + diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt index 715c90c6a3..5bb7f32a1a 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt @@ -51,7 +51,7 @@ class YieldSupplyMinAmountUseCaseTest { fiatAmount = BigDecimal.ZERO, fiatRate = tokenFiatRate, priceChange = BigDecimal.ZERO, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -103,7 +103,7 @@ class YieldSupplyMinAmountUseCaseTest { fiatAmount = null, fiatRate = null, priceChange = null, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -131,7 +131,7 @@ class YieldSupplyMinAmountUseCaseTest { fiatAmount = BigDecimal.ZERO, fiatRate = BigDecimal.ONE, priceChange = BigDecimal.ZERO, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt index c086813f73..5521464a9c 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt @@ -60,7 +60,7 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { fiatAmount = null, fiatRate = BigDecimal.ONE, priceChange = null, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -91,7 +91,7 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { fiatAmount = null, fiatRate = BigDecimal.ONE, priceChange = null, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -136,7 +136,7 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { fiatAmount = null, fiatRate = BigDecimal.ONE, priceChange = null, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -243,7 +243,7 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { fiatAmount = amount.multiply(fiatRate), fiatRate = fiatRate, priceChange = BigDecimal("-0.000058200000000008245"), - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 0b4ac32038..f1162f841b 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -22,7 +22,7 @@ platform :android do FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services/dev/google-services.json", "../app") FileUtils.cp("../tangem-android-tools/CI/gradle_properties/tests_ci_gradle.properties", "../gradle.properties") puts File.read("../gradle.properties") - gradle(task: "detekt detektGoogleDebug detektDebug --continue") + gradle(task: "detekt detektMain") end desc "Run tests" @@ -30,7 +30,7 @@ platform :android do FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services/dev/google-services.json", "../app") FileUtils.cp("../tangem-android-tools/CI/gradle_properties/tests_ci_gradle.properties", "../gradle.properties") puts File.read("../gradle.properties") - gradle(task: "test") + gradle(task: "unitTest") end desc "Build internal APK Firebase App Distribution" diff --git a/features/account/api/detekt-baseline-debug.xml b/features/account/api/detekt-baseline-debug.xml deleted file mode 100644 index 43b883bfc2..0000000000 --- a/features/account/api/detekt-baseline-debug.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - BooleanPropertyNaming:PortfolioFetcher.kt$PortfolioFetcher.Mode.All$val onlyMultiCurrency: Boolean - NonBooleanPropertyPrefixedWithIs:PortfolioSelectorComponent.kt$PortfolioSelectorController$/** * for some Feature specific filtering * combine and update with your Feature data and [PortfolioFetcher.data] */ val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean> - NonBooleanPropertyPrefixedWithIs:PortfolioSelectorComponent.kt$PortfolioSelectorController$val isAccountMode: Flow<Boolean> - UseSumOfInsteadOfFlatMapSize:PortfolioFetcher.kt$PortfolioFetcher.Data$flatten() - - diff --git a/features/account/api/src/main/java/com/tangem/features/account/PortfolioFetcher.kt b/features/account/api/src/main/java/com/tangem/features/account/PortfolioFetcher.kt index 1884981a50..a56d06a5dd 100644 --- a/features/account/api/src/main/java/com/tangem/features/account/PortfolioFetcher.kt +++ b/features/account/api/src/main/java/com/tangem/features/account/PortfolioFetcher.kt @@ -26,7 +26,7 @@ interface PortfolioFetcher { val isSingleChoice: Boolean = balances.values .map { it.accountsBalance.accountStatuses } - .flatten().size == 1 + .sumOf { it.size } == 1 fun isSingleChoice(walletId: UserWalletId): Boolean = balances[walletId] ?.accountsBalance @@ -43,7 +43,7 @@ interface PortfolioFetcher { } sealed interface Mode { - data class All(val onlyMultiCurrency: Boolean) : Mode + data class All(val isOnlyMultiCurrency: Boolean) : Mode data class Wallet(val walletId: UserWalletId) : Mode } diff --git a/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt index 0b98d6b218..50c01a8d05 100644 --- a/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt +++ b/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt @@ -57,6 +57,7 @@ interface PortfolioSelectorController { * combine and update with your Feature data and [PortfolioFetcher.data] */ val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean> + suspend fun isAccountModeSync(): Boolean fun selectAccount(accountId: AccountId?) fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow?> diff --git a/features/account/impl/detekt-baseline-debug.xml b/features/account/impl/detekt-baseline-debug.xml index dce6c95e71..ecf2e0cce8 100644 --- a/features/account/impl/detekt-baseline-debug.xml +++ b/features/account/impl/detekt-baseline-debug.xml @@ -1,10 +1,5 @@ - - NonBooleanPropertyPrefixedWithIs:DefaultPortfolioSelectorController.kt$DefaultPortfolioSelectorController$override val isAccountMode: Flow<Boolean> by lazy { isAccountsModeEnabledUseCase() } - NonBooleanPropertyPrefixedWithIs:DefaultPortfolioSelectorController.kt$DefaultPortfolioSelectorController$override val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean> = MutableStateFlow { _, _ -> true } - NonBooleanPropertyPrefixedWithIs:DefaultPortfolioSelectorController.kt$DefaultPortfolioSelectorController$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:PortfolioSelectorModel.kt$PortfolioSelectorModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - + diff --git a/features/account/impl/src/main/java/com/tangem/features/account/analytics/AccountSettingsAnalyticEvents.kt b/features/account/impl/src/main/java/com/tangem/features/account/analytics/AccountSettingsAnalyticEvents.kt new file mode 100644 index 0000000000..dc8afe9786 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/analytics/AccountSettingsAnalyticEvents.kt @@ -0,0 +1,100 @@ +package com.tangem.features.account.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.features.account.AccountCreateEditComponent + +sealed class AccountSettingsAnalyticEvents( + category: String = "Settings / Account", + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(category, event, params) { + + class AccountSettingsScreenOpened : AccountSettingsAnalyticEvents( + event = "Account Settings Screen Opened", + ) + + class ButtonManageTokens : AccountSettingsAnalyticEvents( + event = "Button - Manage Tokens", + ) + + class ButtonArchiveAccount : AccountSettingsAnalyticEvents( + event = "Button - Archive Account", + ) + + class ButtonArchiveAccountConfirmation : AccountSettingsAnalyticEvents( + event = "Button - Archive Account Confirmation", + ) + + class ButtonCancelAccountArchivation : AccountSettingsAnalyticEvents( + event = "Button - Cancel Account Archivation", + ) + + class AccountArchived : AccountSettingsAnalyticEvents( + event = "Account Archived", + ) + + class ButtonEdit : AccountSettingsAnalyticEvents( + event = "Button - Edit", + ) + + class AccountEditScreenOpened : AccountSettingsAnalyticEvents( + event = "Account Edit Screen Opened", + ) + + class ButtonSave( + val name: AccountName, + val icon: CryptoPortfolioIcon, + ) : AccountSettingsAnalyticEvents( + event = "Button - Save", + params = buildMap { + val accountName = when (name) { + is AccountName.Custom -> name.value + AccountName.DefaultMain -> "DefaultMain" + } + put("Name", accountName) + put("Color", icon.color.name) + put("Icon", icon.value.name) + }, + ) + + class ButtonAddNewAccount( + val name: AccountName, + val icon: CryptoPortfolioIcon, + val derivationIndex: Int, + ) : AccountSettingsAnalyticEvents( + event = "Button - Add New Account", + params = buildMap { + val accountName = when (name) { + is AccountName.Custom -> name.value + AccountName.DefaultMain -> "DefaultMain" + } + put("Name", accountName) + put("Color", icon.color.name) + put("Icon", icon.value.name) + put("Derivation", derivationIndex.toString()) + }, + ) + + class AccountError( + val source: Source, + val error: String, + ) : AccountSettingsAnalyticEvents( + event = "Account Error", + params = buildMap { + put("Error", error) + }, + ) + + enum class Source(val value: String) { + NEW_ACCOUNT("New Account"), EDIT("Edit"), ARCHIVE("Archive") + } + + companion object { + fun AccountCreateEditComponent.Params.toAnalyticSource() = when (this) { + is AccountCreateEditComponent.Params.Create -> Source.NEW_ACCOUNT + is AccountCreateEditComponent.Params.Edit -> Source.EDIT + } + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/analytics/WalletSettingsAccountAnalyticEvents.kt b/features/account/impl/src/main/java/com/tangem/features/account/analytics/WalletSettingsAccountAnalyticEvents.kt new file mode 100644 index 0000000000..165fe01945 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/analytics/WalletSettingsAccountAnalyticEvents.kt @@ -0,0 +1,26 @@ +package com.tangem.features.account.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +sealed class WalletSettingsAccountAnalyticEvents( + category: String = "Settings / Wallet Settings", + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(category, event, params) { + + class AccountCreated : WalletSettingsAccountAnalyticEvents( + event = "Account Created", + ) + + class AccountRecovered : WalletSettingsAccountAnalyticEvents( + event = "Account Recovered", + ) + + class ArchivedAccountsScreenOpened : WalletSettingsAccountAnalyticEvents( + event = "Archived Accounts Screen Opened", + ) + + class ButtonRecoverAccount : WalletSettingsAccountAnalyticEvents( + event = "Button - Recover Account", + ) +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt index 67d7387811..13a3acdaf9 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.account.archived +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.decompose.model.Model @@ -18,6 +19,7 @@ import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase import com.tangem.domain.account.usecase.GetArchivedAccountsUseCase import com.tangem.domain.models.account.AccountId import com.tangem.features.account.ArchivedAccountListComponent +import com.tangem.features.account.analytics.WalletSettingsAccountAnalyticEvents import com.tangem.features.account.archived.entity.AccountArchivedUM import com.tangem.features.account.archived.entity.AccountArchivedUMBuilder import com.tangem.features.account.archived.entity.AccountArchivedUMBuilder.Companion.toggleProgress @@ -40,6 +42,7 @@ internal class ArchivedAccountListModel @Inject constructor( private val recoverCryptoPortfolioUseCase: RecoverCryptoPortfolioUseCase, private val getArchivedAccountsUseCase: GetArchivedAccountsUseCase, private val umBuilder: AccountArchivedUMBuilder, + private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsExceptionHandler: AnalyticsExceptionHandler, ) : Model() { @@ -52,6 +55,7 @@ internal class ArchivedAccountListModel @Inject constructor( private val getArchivedAccountsJob = JobHolder() init { + analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.ArchivedAccountsScreenOpened()) getArchivedAccounts() } @@ -96,6 +100,7 @@ internal class ArchivedAccountListModel @Inject constructor( } private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch { + analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.ButtonRecoverAccount()) uiState.update { it.toggleProgress(accountId, isLoading = true) } val result = withContext(dispatchers.default) { recoverCryptoPortfolioUseCase(accountId) @@ -120,7 +125,7 @@ internal class ArchivedAccountListModel @Inject constructor( messageSender.send( DialogMessage( - title = resourceReference(R.string.account_recover_limit_dialog_title), + title = resourceReference(R.string.common_something_went_wrong), message = resourceReference( id = R.string.account_recover_limit_dialog_description, formatArgs = wrappedList(AccountList.MAX_ACCOUNTS_COUNT.toString()), @@ -148,6 +153,7 @@ internal class ArchivedAccountListModel @Inject constructor( } private fun showSuccessRecoverMessage() { + analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.AccountRecovered()) val message = resourceReference(R.string.account_recover_success_message) messageSender.send(ToastMessage(message = message)) } diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index 08191c3d3f..8720ae4650 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -4,6 +4,7 @@ import androidx.annotation.StringRes import com.tangem.common.ui.account.AccountNameUM import com.tangem.common.ui.account.toDomain import com.tangem.common.ui.account.toUM +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped @@ -25,6 +26,9 @@ import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.account.AccountCreateEditComponent +import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents +import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents.Companion.toAnalyticSource +import com.tangem.features.account.analytics.WalletSettingsAccountAnalyticEvents import com.tangem.features.account.createedit.entity.AccountCreateEditUM import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon @@ -54,6 +58,7 @@ internal class AccountCreateEditModel @Inject constructor( private val addCryptoPortfolioUseCase: AddCryptoPortfolioUseCase, private val getUnoccupiedAccountIndexUseCase: GetUnoccupiedAccountIndexUseCase, private val analyticsExceptionHandler: AnalyticsExceptionHandler, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() @@ -65,6 +70,8 @@ internal class AccountCreateEditModel @Inject constructor( init { if (params is AccountCreateEditComponent.Params.Create) { updateDerivationInfo(userWalletId = params.userWalletId) + } else { + analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountEditScreenOpened()) } } @@ -105,6 +112,12 @@ internal class AccountCreateEditModel @Inject constructor( val icon = state.account.portfolioIcon.toDomain() val index = state.account.derivationInfo.index ?: return val derivationIndex = DerivationIndex(value = index).getOrNull() ?: return + val event = AccountSettingsAnalyticEvents.ButtonAddNewAccount( + name = name, + icon = icon, + derivationIndex = derivationIndex.value, + ) + analyticsEventHandler.send(event) uiState.value = uiState.value.toggleProgress(showProgress = true) val result = addCryptoPortfolioUseCase( @@ -118,12 +131,22 @@ internal class AccountCreateEditModel @Inject constructor( result .onLeft(::handleAddAccountError) .onRight { + analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.AccountCreated()) showMessage(R.string.account_create_success_message) router.pop() } } private fun handleAddAccountError(error: AddCryptoPortfolioUseCase.Error) { + val event = AccountSettingsAnalyticEvents.AccountError( + source = params.toAnalyticSource(), + error = when (error) { + is AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet -> error.cause.tag + is AddCryptoPortfolioUseCase.Error.DataOperationFailed -> error.cause.message.orEmpty() + }, + ) + analyticsEventHandler.send(event) + val isDuplicateAccountNamesError = (error as? AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet) ?.cause is AccountList.Error.DuplicateAccountNames when { @@ -141,6 +164,7 @@ internal class AccountCreateEditModel @Inject constructor( val icon = state.account.portfolioIcon.toDomain() val isNewName = name != params.account.accountName val isNewIcon = icon != params.account.portfolioIcon + analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonSave(name, icon)) uiState.value = uiState.value.toggleProgress(showProgress = true) val result = updateCryptoPortfolioUseCase( @@ -159,6 +183,15 @@ internal class AccountCreateEditModel @Inject constructor( } private fun handleEditAccountError(error: UpdateCryptoPortfolioUseCase.Error) { + val event = AccountSettingsAnalyticEvents.AccountError( + source = params.toAnalyticSource(), + error = when (error) { + is UpdateCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet -> error.cause.tag + is UpdateCryptoPortfolioUseCase.Error.DataOperationFailed -> error.cause.message.orEmpty() + UpdateCryptoPortfolioUseCase.Error.NothingToUpdate -> error::class.simpleName.orEmpty() + }, + ) + analyticsEventHandler.send(event) val isDuplicateAccountNamesError = (error as? UpdateCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet) ?.cause is AccountList.Error.DuplicateAccountNames when { @@ -274,7 +307,7 @@ internal class AccountCreateEditModel @Inject constructor( private fun showSomethingWrong() { val dialogMessage = DialogMessage( title = resourceReference(R.string.common_something_went_wrong), - message = resourceReference(R.string.account_could_not_create), + message = resourceReference(R.string.account_generic_error_dialog_message), ) messageSender.send(dialogMessage) } diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt index 57adf472ad..5d48231c07 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt @@ -1,15 +1,10 @@ package com.tangem.features.account.createedit.ui import android.content.res.Configuration -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -30,6 +25,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachIndexed import com.tangem.common.ui.R import com.tangem.common.ui.account.* import com.tangem.core.ui.components.PrimaryButton @@ -186,9 +183,9 @@ private fun AccountColor(colorsState: AccountCreateEditUM.Colors) { modifier = Modifier .fillMaxWidth() .padding(contentPadding), - horizontalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), ) { - colorsState.list.forEach { color -> + colorsState.list.fastForEach { color -> val isSelected = color == colorsState.selected Box( contentAlignment = Alignment.Center, @@ -236,8 +233,9 @@ private fun AccountIcons(iconsState: AccountCreateEditUM.Icons) { FlowRow( maxItemsInEachRow = 6, modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, ) { - iconsState.list.forEachIndexed { index, icon -> + iconsState.list.fastForEachIndexed { index, icon -> val isSelected = icon == iconsState.selected Box( contentAlignment = Alignment.Center, diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt index 36c4655927..c1bdf53342 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.account.details import com.tangem.common.routing.AppRoute import com.tangem.common.ui.account.toUM +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -20,6 +21,7 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.account.AccountDetailsComponent +import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon import com.tangem.features.account.details.entity.AccountDetailsUM import com.tangem.features.account.details.entity.AccountDetailsUM.ArchiveMode @@ -37,6 +39,7 @@ internal class AccountDetailsModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val archiveCryptoPortfolioUseCase: ArchiveCryptoPortfolioUseCase, singleAccountSupplier: SingleAccountSupplier, + private val analyticsEventHandler: AnalyticsEventHandler, private val getUserWalletUseCase: GetUserWalletUseCase, ) : Model() { @@ -48,12 +51,14 @@ internal class AccountDetailsModel @Inject constructor( private val accountId = params.account.accountId init { + analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountSettingsScreenOpened()) singleAccountSupplier(SingleAccountProducer.Params(accountId)) .onEach { account -> uiState.update { buildUI(account) } } .launchIn(modelScope) } private fun onEditAccountClick(account: Account) { + analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonEdit()) router.push(AppRoute.EditAccount(account)) } @@ -62,17 +67,21 @@ internal class AccountDetailsModel @Inject constructor( source = AppRoute.ManageTokens.Source.SETTINGS, portfolioId = PortfolioId(account.accountId), ) + analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonManageTokens()) router.push(route) } private fun onArchiveAccountClick() { + analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonArchiveAccount()) confirmArchiveDialog() } private fun confirmArchiveDialog() { val secondAction = EventMessageAction( title = resourceReference(R.string.common_cancel), - onClick = {}, + onClick = { + analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonCancelAccountArchivation()) + }, ) val firstAction = EventMessageAction( title = resourceReference(R.string.account_details_archive_action), @@ -90,6 +99,7 @@ internal class AccountDetailsModel @Inject constructor( } private fun archiveCryptoPortfolio() = modelScope.launch { + analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonArchiveAccountConfirmation()) uiState.update { it.toggleProgress(true) } archiveCryptoPortfolioUseCase(accountId) .onLeft { error -> @@ -97,6 +107,7 @@ internal class AccountDetailsModel @Inject constructor( uiState.update { it.toggleProgress(false) } } .onRight { + analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountArchived()) val message = resourceReference(R.string.account_archive_success_message) messageSender.send(ToastMessage(message = message)) router.pop() @@ -104,21 +115,18 @@ internal class AccountDetailsModel @Inject constructor( } private fun failedArchiveDialog(error: ArchiveCryptoPortfolioUseCase.Error) { - val titleRes = when (error) { - is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet, - is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountNotFound, - is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountsNotCreated, - is ArchiveCryptoPortfolioUseCase.Error.DataOperationFailed, - -> R.string.common_something_went_wrong - is ArchiveCryptoPortfolioUseCase.Error.ActiveReferralStatus, - -> R.string.account_could_not_archive_referral_program_title - } + val event = AccountSettingsAnalyticEvents.AccountError( + source = AccountSettingsAnalyticEvents.Source.ARCHIVE, + error = error.tag, + ) + analyticsEventHandler.send(event) + val titleRes = R.string.common_something_went_wrong val messageRes = when (error) { is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet, is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountNotFound, is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountsNotCreated, is ArchiveCryptoPortfolioUseCase.Error.DataOperationFailed, - -> R.string.account_could_not_archive + -> R.string.account_generic_error_dialog_message is ArchiveCryptoPortfolioUseCase.Error.ActiveReferralStatus, -> R.string.account_could_not_archive_referral_program_message } diff --git a/features/account/impl/src/main/java/com/tangem/features/account/fetcher/DefaultPortfolioFetcher.kt b/features/account/impl/src/main/java/com/tangem/features/account/fetcher/DefaultPortfolioFetcher.kt index fcdd27ea9d..f8bf368a40 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/fetcher/DefaultPortfolioFetcher.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/fetcher/DefaultPortfolioFetcher.kt @@ -72,7 +72,7 @@ internal class DefaultPortfolioFetcher @AssistedInject constructor( private fun List.filterWallets(mode: Mode): List = this.filter { wallet -> when (mode) { - is Mode.All -> if (mode.onlyMultiCurrency) wallet.isMultiCurrency else true + is Mode.All -> if (mode.isOnlyMultiCurrency) wallet.isMultiCurrency else true is Mode.Wallet -> wallet.walletId == mode.walletId } } diff --git a/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt b/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt index 00b2a52f71..fe9a055c3f 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt @@ -29,6 +29,8 @@ internal class DefaultPortfolioSelectorController @Inject constructor( override val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean> = MutableStateFlow { _, _ -> true } + override suspend fun isAccountModeSync(): Boolean = isAccountsModeEnabledUseCase.invokeSync() + override fun selectAccount(accountId: AccountId?) { _selectedAccount.tryEmit(accountId) } diff --git a/features/biometry/impl/detekt-baseline-debug.xml b/features/biometry/impl/detekt-baseline-debug.xml deleted file mode 100644 index f439e02f2e..0000000000 --- a/features/biometry/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - BooleanPropertyNaming:AskBiometryUM.kt$AskBiometryUM$val bottomSheetVariant: Boolean = false - BooleanPropertyNaming:AskBiometryUM.kt$AskBiometryUM$val showProgress: Boolean = false - BooleanPropertyNaming:DefaultAskBiometryComponent.kt$DefaultAskBiometryComponent$val bsShown by bsShown.collectAsStateWithLifecycle() - MultilineLambdaItParameter:AskBiometryModel.kt$AskBiometryModel${ uiMessageSender.send( SnackbarMessage(stringReference("Something went wrong. Please contact support: $it")), ) } - - diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/DefaultAskBiometryComponent.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/DefaultAskBiometryComponent.kt index 81ca241043..4fb9e79cd1 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/DefaultAskBiometryComponent.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/DefaultAskBiometryComponent.kt @@ -41,10 +41,10 @@ internal class DefaultAskBiometryComponent @AssistedInject constructor( @Composable override fun BottomSheet() { val state by model.uiState.collectAsStateWithLifecycle() - val bsShown by bsShown.collectAsStateWithLifecycle() - val bsConfig = remember(this, bsShown) { + val isBSShown by bsShown.collectAsStateWithLifecycle() + val bsConfig = remember(this, isBSShown) { TangemBottomSheetConfig( - isShown = bsShown, + isShown = isBSShown, onDismissRequest = ::dismiss, content = TangemBottomSheetConfigContent.Empty, ) diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt index b6f43fdbbe..2d586fad73 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt @@ -55,7 +55,7 @@ internal class AskBiometryModel @Inject constructor( private val _uiState = MutableStateFlow( AskBiometryUM( - bottomSheetVariant = params.isBottomSheetVariant, + isBottomSheetVariant = params.isBottomSheetVariant, onAllowClick = ::onAllowClick, onDontAllowClick = ::dontAllow, onDismiss = ::dismiss, @@ -85,7 +85,7 @@ internal class AskBiometryModel @Inject constructor( return@launch } - _uiState.update { it.copy(showProgress = true) } + _uiState.update { it.copy(shouldShowProgress = true) } /* @@ -96,7 +96,7 @@ internal class AskBiometryModel @Inject constructor( uiMessageSender.send( SnackbarMessage(stringReference("No selected user wallet")), ) - _uiState.update { it.copy(showProgress = false) } + _uiState.update { it.copy(shouldShowProgress = false) } return@launch } @@ -128,7 +128,7 @@ internal class AskBiometryModel @Inject constructor( } } - if (_uiState.value.bottomSheetVariant) { + if (_uiState.value.isBottomSheetVariant) { dismissBSFlow.emit(Unit) delay(timeMillis = 500) } @@ -142,9 +142,9 @@ internal class AskBiometryModel @Inject constructor( userWalletId = userWallet.walletId, lockMethod = UserWalletsListRepository.LockMethod.Biometric, changeUnsecured = false, - ).onLeft { + ).onLeft { error -> uiMessageSender.send( - SnackbarMessage(stringReference("Something went wrong. Please contact support: $it")), + SnackbarMessage(stringReference("Something went wrong. Please contact support: $error")), ) } } diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/AskBiometry.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/AskBiometry.kt index cd367b9850..a3726bf381 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/AskBiometry.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/AskBiometry.kt @@ -29,7 +29,7 @@ internal fun AskBiometry(state: AskBiometryUM, modifier: Modifier = Modifier) { Column( modifier = Modifier.weight(1f), ) { - if (state.bottomSheetVariant) { + if (state.isBottomSheetVariant) { Header(onCloseClick = state.onDismiss) } @@ -128,12 +128,12 @@ private fun Footer(state: AskBiometryUM, modifier: Modifier = Modifier) { ) { PrimaryButton( modifier = Modifier.fillMaxWidth(), - showProgress = state.showProgress, + showProgress = state.shouldShowProgress, text = stringResourceSafe(id = R.string.save_user_wallet_agreement_allow_biometrics), onClick = state.onAllowClick, ) - if (state.bottomSheetVariant.not()) { + if (state.isBottomSheetVariant.not()) { SpacerH12() SecondaryButton( @@ -199,7 +199,7 @@ private fun Preview() { private fun PreviewBS() { TangemThemePreview { AskBiometry( - state = AskBiometryUM(bottomSheetVariant = true), + state = AskBiometryUM(isBottomSheetVariant = true), ) } } \ No newline at end of file diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/state/AskBiometryUM.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/state/AskBiometryUM.kt index 0148aba873..88ed6c696b 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/state/AskBiometryUM.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/ui/state/AskBiometryUM.kt @@ -3,8 +3,8 @@ package com.tangem.features.biometry.impl.ui.state import com.tangem.core.ui.extensions.TextReference internal data class AskBiometryUM( - val bottomSheetVariant: Boolean = false, - val showProgress: Boolean = false, + val isBottomSheetVariant: Boolean = false, + val shouldShowProgress: Boolean = false, val error: TextReference? = null, val onAllowClick: () -> Unit = {}, val onDontAllowClick: () -> Unit = {}, diff --git a/features/create-wallet-selection/impl/build.gradle.kts b/features/create-wallet-selection/impl/build.gradle.kts index da58579502..4f1fb170a2 100644 --- a/features/create-wallet-selection/impl/build.gradle.kts +++ b/features/create-wallet-selection/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.wallets) implementation(projects.domain.models) + implementation(projects.domain.hotWallet) /** Core modules */ implementation(projects.core.configToggles) diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt index bc586d7d95..2ee891e036 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt @@ -2,15 +2,20 @@ package com.tangem.features.createwalletselection import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic +import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent +import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.components.label.entity.LabelStyle import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.card.analytics.IntroductionProcess -import com.tangem.domain.card.analytics.Shop +import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog +import com.tangem.domain.hotwallet.IsHotWalletCreationSupported import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM import com.tangem.features.createwalletselection.impl.R @@ -28,9 +33,12 @@ import javax.inject.Inject internal class CreateWalletSelectionModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, + private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, private val urlOpener: UrlOpener, + private val isHotWalletCreationSupported: IsHotWalletCreationSupported, + private val uiMessageSender: UiMessageSender, ) : Model() { internal val uiState: StateFlow @@ -93,7 +101,20 @@ internal class CreateWalletSelectionModel @Inject constructor( } private fun onMobileWalletClick() { - router.push(AppRoute.CreateMobileWallet) + trackingContextProxy.addHotWalletContext() + analyticsEventHandler.send( + event = OnboardingAnalyticsEvent.Onboarding.ButtonMobileWallet( + source = AnalyticsParam.ScreensSources.AddNewWallet.value, + ), + ) + if (!isHotWalletCreationSupported()) { + uiMessageSender.send( + hotWalletCreationNotSupportedDialog(isHotWalletCreationSupported.getLeastVersionName()), + ) + return + } + + router.push(AppRoute.CreateMobileWallet(AnalyticsParam.ScreensSources.AddNewWallet.value)) } private fun onHardwareWalletClick() { @@ -101,8 +122,7 @@ internal class CreateWalletSelectionModel @Inject constructor( } private fun onBuyClick() { - analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards) - analyticsEventHandler.send(Shop.ScreenOpened) + analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.AddNewWallet)) modelScope.launch { generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } } diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt index 7fde4ee3ac..07cdb0689e 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -137,11 +138,12 @@ private fun WalletBlock( vertical = 12.dp, ), ) { - Row { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { Text( modifier = Modifier - .weight(1f, fill = false) - .padding(end = 8.dp), + .weight(1f, fill = false), text = title, style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, diff --git a/features/create-wallet-start/impl/build.gradle.kts b/features/create-wallet-start/impl/build.gradle.kts index 6909f9d54c..8552f7b7d9 100644 --- a/features/create-wallet-start/impl/build.gradle.kts +++ b/features/create-wallet-start/impl/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.wallets) implementation(projects.domain.models) + implementation(projects.domain.hotWallet) /** Core modules */ implementation(projects.core.configToggles) diff --git a/features/create-wallet-start/impl/detekt-baseline-debug.xml b/features/create-wallet-start/impl/detekt-baseline-debug.xml deleted file mode 100644 index 7090dd6ebf..0000000000 --- a/features/create-wallet-start/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - BooleanPropertyNaming:CreateWalletStartUM.kt$CreateWalletStartUM$val showScanSecondaryButton: Boolean - MultilineLambdaItParameter:CreateWalletStartContent.kt${ FeatureItem( iconResId = it.iconResId, text = it.text, ) } - MultilineLambdaItParameter:CreateWalletStartModel.kt$CreateWalletStartModel${ delay(HIDE_PROGRESS_DELAY) setLoading(false) when (it) { is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -> { userWalletsListRepository.unlock( userWalletId = userWallet.walletId, unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), ).onRight { appRouter.replaceAll(AppRoute.Wallet) } } } } - - diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt index c0051682a8..ef8ef65b13 100644 --- a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt @@ -6,8 +6,9 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic.SignedIn -import com.tangem.core.analytics.models.Basic.SignedIn.SignInType +import com.tangem.core.analytics.models.Basic +import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent +import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -18,12 +19,13 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog import com.tangem.domain.card.ScanCardProcessor -import com.tangem.domain.card.analytics.ParamCardCurrencyConverter -import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.analytics.IntroductionProcess import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.error.SaveWalletError +import com.tangem.domain.hotwallet.IsHotWalletCreationSupported import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.builder.ColdUserWalletBuilder @@ -51,14 +53,16 @@ internal class CreateWalletStartModel @Inject constructor( private val scanCardProcessor: ScanCardProcessor, private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsRepository: SettingsRepository, - private val analyticsEventHandler: AnalyticsEventHandler, private val appRouter: AppRouter, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, private val saveWalletUseCase: SaveWalletUseCase, + private val isHotWalletCreationSupported: IsHotWalletCreationSupported, private val userWalletsListRepository: UserWalletsListRepository, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, private val urlOpener: UrlOpener, + private val trackingContextProxy: TrackingContextProxy, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() @@ -84,7 +88,7 @@ internal class CreateWalletStartModel @Inject constructor( ), ), imageResId = R.drawable.img_hardware_wallet, - showScanSecondaryButton = true, + shouldShowScanSecondaryButton = true, onPrimaryButtonClick = ::onBuyClick, primaryButtonText = resourceReference(R.string.details_buy_wallet), otherMethodTitle = resourceReference(R.string.welcome_create_wallet_mobile_title), @@ -112,7 +116,7 @@ internal class CreateWalletStartModel @Inject constructor( ), ), imageResId = R.drawable.img_mobile_wallet, - showScanSecondaryButton = false, + shouldShowScanSecondaryButton = false, onPrimaryButtonClick = ::onStartWithMobileWalletClick, primaryButtonText = resourceReference(R.string.welcome_create_wallet_mobile_title), otherMethodTitle = resourceReference(R.string.welcome_create_wallet_use_hardware_title), @@ -125,15 +129,38 @@ internal class CreateWalletStartModel @Inject constructor( }, ) + init { + analyticsEventHandler.send( + event = IntroductionProcess.CreateWalletIntroScreenOpened(), + ) + } + private fun onScanClick() { + analyticsEventHandler.send( + event = IntroductionProcess.ButtonScanCard(AnalyticsParam.ScreensSources.CreateWalletIntro), + ) scanCard() } private fun onStartWithMobileWalletClick() { - router.push(AppRoute.CreateMobileWallet) + trackingContextProxy.addHotWalletContext() + analyticsEventHandler.send( + event = OnboardingAnalyticsEvent.Onboarding.ButtonMobileWallet( + source = AnalyticsParam.ScreensSources.CreateWalletIntro.value, + ), + ) + if (!isHotWalletCreationSupported()) { + uiMessageSender.send( + hotWalletCreationNotSupportedDialog(isHotWalletCreationSupported.getLeastVersionName()), + ) + return + } + + router.push(AppRoute.CreateMobileWallet(AnalyticsParam.ScreensSources.CreateWalletIntro.value)) } private fun onBuyClick() { + analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.CreateWalletIntro)) modelScope.launch { generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } } @@ -182,11 +209,11 @@ internal class CreateWalletStartModel @Inject constructor( } saveWalletUseCase(userWallet = userWallet).fold( - ifLeft = { + ifLeft = { error -> delay(HIDE_PROGRESS_DELAY) setLoading(false) - when (it) { - is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + when (error) { + is SaveWalletError.DataError -> Timber.e(error.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -> { userWalletsListRepository.unlock( userWalletId = userWallet.walletId, @@ -199,28 +226,11 @@ internal class CreateWalletStartModel @Inject constructor( }, ifRight = { setLoading(false) - sendSignedInCardAnalyticsEvent(scanResponse = scanResponse, isImported = userWallet.isImported) appRouter.replaceAll(AppRoute.Wallet) }, ) } - private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse, isImported: Boolean) { - val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) - if (currency != null) { - analyticsEventHandler.send( - SignedIn( - currency = currency, - batch = scanResponse.card.batchId, - signInType = SignInType.Card, - walletsCount = userWalletsListRepository.userWalletsSync().size.toString(), - isImported = isImported, - hasBackup = scanResponse.card.backupStatus?.isActive, - ), - ) - } - } - private fun setLoading(isLoading: Boolean) { uiState.update { it.copy(isScanInProgress = isLoading) } } diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/entity/CreateWalletStartUM.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/entity/CreateWalletStartUM.kt index 58f58d2bc0..b45777a20a 100644 --- a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/entity/CreateWalletStartUM.kt +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/entity/CreateWalletStartUM.kt @@ -9,7 +9,7 @@ internal data class CreateWalletStartUM( val featureItems: ImmutableList, val imageResId: Int, val isScanInProgress: Boolean, - val showScanSecondaryButton: Boolean, + val shouldShowScanSecondaryButton: Boolean, val primaryButtonText: TextReference, val onPrimaryButtonClick: () -> Unit, val otherMethodDescription: TextReference, diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/ui/CreateWalletStartContent.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/ui/CreateWalletStartContent.kt index 64070ed61e..ecf741b0f0 100644 --- a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/ui/CreateWalletStartContent.kt +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/ui/CreateWalletStartContent.kt @@ -120,10 +120,10 @@ internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modi horizontalArrangement = Arrangement.Center, verticalArrangement = Arrangement.spacedBy(8.dp), ) { - state.featureItems.forEach { + state.featureItems.forEach { item -> FeatureItem( - iconResId = it.iconResId, - text = it.text, + iconResId = item.iconResId, + text = item.text, ) } } @@ -143,7 +143,7 @@ internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modi ) }, bottomContent = { - if (state.showScanSecondaryButton) { + if (state.shouldShowScanSecondaryButton) { SecondaryButtonIconEnd( modifier = Modifier .fillMaxWidth() @@ -232,7 +232,7 @@ internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modi minImageHeight = 160.dp, ) } - if (!state.showScanSecondaryButton) { + if (!state.shouldShowScanSecondaryButton) { FlowRow( modifier = Modifier .wrapContentWidth() @@ -425,7 +425,7 @@ private class CreateWalletStartStateProvider : CollectionPreviewParameterProvide ), ), imageResId = R.drawable.img_hardware_wallet, - showScanSecondaryButton = true, + shouldShowScanSecondaryButton = true, onPrimaryButtonClick = { }, primaryButtonText = resourceReference(R.string.details_buy_wallet), otherMethodTitle = resourceReference(R.string.welcome_create_wallet_mobile_title), @@ -455,7 +455,7 @@ private class CreateWalletStartStateProvider : CollectionPreviewParameterProvide ), ), imageResId = R.drawable.img_mobile_wallet, - showScanSecondaryButton = false, + shouldShowScanSecondaryButton = false, onPrimaryButtonClick = { }, primaryButtonText = resourceReference(R.string.welcome_create_wallet_mobile_title), otherMethodTitle = resourceReference(R.string.welcome_create_wallet_use_hardware_title), diff --git a/features/details/impl/detekt-baseline-debug.xml b/features/details/impl/detekt-baseline-debug.xml index e574008e71..59c8448c09 100644 --- a/features/details/impl/detekt-baseline-debug.xml +++ b/features/details/impl/detekt-baseline-debug.xml @@ -9,7 +9,6 @@ MultilineLambdaItParameter:DetailsModel.kt$DetailsModel${ it.copy( selectFeedbackEmailTypeBSConfig = it.selectFeedbackEmailTypeBSConfig.copy(isShown = false), ) } MultilineLambdaItParameter:PreviewUserWalletListComponent.kt$PreviewUserWalletListComponent${ it.copy( balance = UserWalletItemUM.Balance.Loaded( value = "1.000 BTC", isFlickering = true, ), ) } MultilineLambdaItParameter:UserWalletSaver.kt$UserWalletSaver${ val message = it.message if (!message.isNullOrEmpty()) { messageSender.send(SnackbarMessage(message)) } } - NonBooleanPropertyPrefixedWithIs:UserWalletListModel.kt$UserWalletListModel$private val isWalletSavingInProgress: MutableStateFlow<Boolean> = MutableStateFlow(value = false) RedundantSuspendModifier:UserWalletSaver.kt$UserWalletSaver$suspend UnnecessaryLet:ItemsBuilder.kt$ItemsBuilder$let(::add) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index cf7047bb68..be5d74ca8e 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -4,6 +4,9 @@ import android.content.res.Resources import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.AppInstanceIdProvider +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -13,13 +16,14 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.card.common.TapWorkarounds.isVisa import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase -import com.tangem.domain.feedback.models.WalletMetaInfo import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.feedback.models.WalletMetaInfo import com.tangem.domain.feedback.repository.FeedbackFeatureToggles +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase -import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.details.component.DetailsComponent @@ -29,6 +33,7 @@ import com.tangem.features.details.entity.DetailsUM import com.tangem.features.details.entity.SelectEmailFeedbackTypeBS import com.tangem.features.details.utils.ItemsBuilder import com.tangem.features.details.utils.SocialsBuilder +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.version.AppVersionProvider import kotlinx.collections.immutable.ImmutableList @@ -60,6 +65,9 @@ internal class DetailsModel @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, private val feedbackFeatureToggles: FeedbackFeatureToggles, override val dispatchers: CoroutineDispatcherProvider, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params: DetailsComponent.Params = paramsContainer.require() @@ -216,7 +224,12 @@ internal class DetailsModel @Inject constructor( private fun onBuyClick() { modelScope.launch { - urlOpener.openUrl(buildBuyLink()) + if (hotWalletFeatureToggles.isHotWalletEnabled) { + analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Settings)) + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } else { + urlOpener.openUrl(buildBuyLink()) + } } } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index 923627f080..f0b9432ebf 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -3,6 +3,9 @@ package com.tangem.features.details.model import com.tangem.common.routing.AppRoute import com.tangem.common.ui.userwallet.handle import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.event.SignIn import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router @@ -39,6 +42,7 @@ internal class UserWalletListModel @Inject constructor( private val userWalletSaver: UserWalletSaver, private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val unlockWalletUseCase: UnlockWalletUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val isWalletSavingInProgress: MutableStateFlow = MutableStateFlow(value = false) @@ -87,6 +91,7 @@ internal class UserWalletListModel @Inject constructor( private fun onAddNewWalletClick() { if (hotWalletFeatureToggles.isHotWalletEnabled) { + analyticsEventHandler.send(SignIn.ButtonAddWallet(AnalyticsParam.ScreensSources.SignIn)) router.push(AppRoute.CreateWalletSelection) } else { withProgress(isWalletSavingInProgress) { @@ -105,6 +110,7 @@ internal class UserWalletListModel @Inject constructor( error.handle( onUserCancelled = {}, onAlreadyUnlocked = { router.push(AppRoute.WalletSettings(userWalletId)) }, + analyticsEventHandler = analyticsEventHandler, showMessage = messageSender::send, ) } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt index 0bd950c5c8..6be90f525b 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt @@ -185,7 +185,9 @@ private fun Footer(model: DetailsFooterUM, modifier: Modifier = Modifier) { } Text( - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6), + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing6) + .testTag(DetailsScreenTestTags.VERSION_NAME), text = model.appVersion, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt index ad7aab9506..43d68d3d11 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.features.feed.entry.BottomSheetState +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState @Stable interface FeedEntryComponent { diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts index c309d45ccf..cadd191a0e 100644 --- a/features/feed/impl/build.gradle.kts +++ b/features/feed/impl/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.notifications.models) implementation(projects.domain.transaction) + implementation(projects.domain.news) // FIXME [REDACTED_TASK_KEY] // Remove the "Buy" and "Sell" actions from the redux middleware. @@ -86,6 +87,7 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.navigation) + implementation(projects.core.utils) /* Common */ implementation(projects.common.ui) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt index cdf3a46598..d27de2ff81 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt @@ -1,10 +1,13 @@ package com.tangem.features.feed.components +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.State +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp +import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack @@ -13,11 +16,13 @@ import com.arkivanov.decompose.value.Value import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.navigation.inner.InnerRouter +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState +import com.tangem.core.ui.decompose.ComposableModularContentComponent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent -import com.tangem.features.feed.entry.BottomSheetState import com.tangem.features.feed.entry.components.FeedEntryComponent +import com.tangem.features.feed.ui.EntryBottomSheetContent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -35,7 +40,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( popCallback = { onChildBack() }, ) - private val stack: Value> = childStack( + private val stack: Value> = childStack( key = "main", source = stackNavigation, serializer = FeedEntryChildFactory.Child.serializer(), @@ -59,7 +64,16 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier, ) { - bottomSheetState // TODO will be continued in next tasks. + val stackState by stack.subscribeAsState() + + BackHandler(enabled = bottomSheetState.value == BottomSheetState.EXPANDED) { + onChildBack() + } + + EntryBottomSheetContent( + stackState = stackState, + onHeaderSizeChange = onHeaderSizeChange, + ) } private fun marketsListTokenSelected(token: TokenMarketParams, appCurrency: AppCurrency) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt index c33781008b..6483e57566 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt @@ -3,6 +3,7 @@ package com.tangem.features.feed.components import androidx.compose.runtime.Immutable import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.navigation.Route +import com.tangem.core.ui.decompose.ComposableModularContentComponent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.features.feed.components.feed.DefaultFeedComponent @@ -11,8 +12,9 @@ import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListCo import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent import com.tangem.features.feed.components.news.list.DefaultNewsListComponent import kotlinx.serialization.Serializable +import javax.inject.Inject -internal class FeedEntryChildFactory { +internal class FeedEntryChildFactory @Inject constructor() { @Serializable @Immutable @@ -43,7 +45,7 @@ internal class FeedEntryChildFactory { child: Child, appComponentContext: AppComponentContext, onTokenClick: (TokenMarketParams, AppCurrency) -> Unit, - ): Any { + ): ComposableModularContentComponent { return when (child) { is Child.TokenDetails -> { DefaultMarketsTokenDetailsComponent( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt index a6bd119f49..910542c05a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt @@ -1,23 +1,37 @@ package com.tangem.features.feed.components.feed import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableModularContentComponent +import com.tangem.features.feed.model.feed.FeedComponentModel +import com.tangem.features.feed.ui.feed.FeedListContent +import com.tangem.features.feed.ui.feed.FeedListHeader internal class DefaultFeedComponent( appComponentContext: AppComponentContext, ) : ComposableModularContentComponent, AppComponentContext by appComponentContext { + private val feedComponentModel = getOrCreateModel() + @Composable override fun Title() { + val state by feedComponentModel.state.collectAsStateWithLifecycle() + FeedListHeader(state.searchBar) } @Composable override fun Content(modifier: Modifier) { + val state by feedComponentModel.state.collectAsStateWithLifecycle() + FeedListContent( + modifier = modifier, + state = state, + ) } @Composable - override fun Footer() { - } + override fun Footer() = Unit } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ComponentModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ComponentModule.kt new file mode 100644 index 0000000000..5c06261458 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.feed.di + +import com.tangem.features.feed.components.DefaultFeedEntryComponent +import com.tangem.features.feed.entry.components.FeedEntryComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindFeedEntryComponent(factory: DefaultFeedEntryComponent.Factory): FeedEntryComponent.Factory +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt new file mode 100644 index 0000000000..f00b75a4b1 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.feed.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.feed.model.feed.FeedComponentModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(FeedComponentModel::class) + fun bindsFeedComponentModel(model: FeedComponentModel): Model +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt new file mode 100644 index 0000000000..5445a03eed --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt @@ -0,0 +1,103 @@ +package com.tangem.features.feed.model.feed + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase +import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase +import com.tangem.features.feed.impl.R +import com.tangem.features.feed.ui.feed.state.* +import com.tangem.features.feed.ui.market.state.SortByTypeUM +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toPersistentHashMap +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import javax.inject.Inject + +@Stable +@ModelScoped +internal class FeedComponentModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val fetchTrendingNewsUseCase: FetchTrendingNewsUseCase, + private val manageTrendingNewsUseCase: ManageTrendingNewsUseCase, +) : Model() { + + private val _state = MutableStateFlow(initialState()) + val state = _state.asStateFlow() + + private val searchBarStateFactory by lazy(LazyThreadSafetyMode.NONE) { + SearchBarStateFactory( + currentStateProvider = Provider { _state.value }, + onStateUpdate = { newState -> _state.update { newState } }, + ) + } + + private val trendingNewsStateFactory by lazy(LazyThreadSafetyMode.NONE) { + TrendingNewsStateFactory( + currentStateProvider = Provider { _state.value }, + onStateUpdate = { newState -> _state.update { newState } }, + ) + } + + init { + modelScope.launch(dispatchers.default) { + fetchTrendingNewsUseCase() + subscribeOnTrendingNews() + } + _state.update { feedListUM -> + feedListUM.copy( + searchBar = _state.value.searchBar.copy(onQueryChange = searchBarStateFactory::onSearchQueryChange), + ) + } + } + + private suspend fun subscribeOnTrendingNews() { + manageTrendingNewsUseCase().collect { articles -> + trendingNewsStateFactory.updateTrendingNewsState(articles) + } + } + + private fun initialState(): FeedListUM { + return FeedListUM( + currentDate = getCurrentDate(), + searchBar = SearchBarUM( + placeholderText = resourceReference(R.string.markets_search_header_title), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = { }, + ), + feedListCallbacks = FeedListCallbacks( + onSearchClick = {}, + onMarketOpenClick = {}, + onArticleClick = {}, + onOpenAllNews = {}, + onMarketItemClick = {}, + onSortTypeClick = {}, + ), + news = NewsUM.Loading, + trendingArticle = null, + marketChartConfig = MarketChartConfig( + marketCharts = buildMap { + SortByTypeUM.entries.forEach { + put(it, MarketChartUM.Loading) + } // TODO in [REDACTED_TASK_KEY] add correct sorting + }.toPersistentHashMap(), + currentSortByType = SortByTypeUM.TopGainers, + ), + ) + } + + private fun getCurrentDate(): String { + val localDate = DateTime(DateTime.now(), DateTimeZone.getDefault()) + return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryBottomSheetContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryBottomSheetContent.kt new file mode 100644 index 0000000000..cf87b0cefd --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryBottomSheetContent.kt @@ -0,0 +1,48 @@ +package com.tangem.features.feed.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.decompose.ComposableModularContentComponent +import com.tangem.features.feed.components.FeedEntryChildFactory + +@Composable +internal fun EntryBottomSheetContent( + stackState: ChildStack, + onHeaderSizeChange: (Dp) -> Unit, +) { + val density = LocalDensity.current + + Scaffold( + contentWindowInsets = WindowInsets(0.dp), + topBar = { + AnimatedContent( + targetState = stackState.active.instance, + modifier = Modifier.onGloballyPositioned { coordinates -> + if (coordinates.size.height > 0) { + with(density) { + onHeaderSizeChange(coordinates.size.height.toDp()) + } + } + }, + ) { currentState -> + currentState.Title() + } + }, + content = { contentPadding -> + AnimatedContent( + stackState.active.instance, + ) { currentState -> + currentState.Content(modifier = Modifier.padding(contentPadding)) + } + }, + ) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt index 51c97516b7..82cde520e1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt @@ -22,25 +22,25 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach +import com.tangem.common.ui.news.ArticleCard +import com.tangem.common.ui.news.ArticleConfigUM import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.fields.SearchBar -import com.tangem.common.ui.news.ArticleCard -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.core.ui.components.fields.TangemSearchBarDefaults +import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe @@ -48,51 +48,44 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState -import com.tangem.features.feed.ui.feed.state.FeedListCallbacks -import com.tangem.features.feed.ui.feed.state.FeedListUM -import com.tangem.features.feed.ui.feed.state.MarketChartConfig -import com.tangem.features.feed.ui.feed.state.MarketChartUM +import com.tangem.features.feed.ui.feed.state.* import com.tangem.features.feed.ui.market.components.MarketsListItem import com.tangem.features.feed.ui.market.components.MarketsListItemPlaceholder import com.tangem.features.feed.ui.market.state.MarketsListItemUM import com.tangem.features.feed.ui.market.state.SortByTypeUM -import kotlinx.collections.immutable.ImmutableList @Composable -internal fun FeedList(state: FeedListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) { - val density = LocalDensity.current +internal fun FeedListHeader(searchBarUM: SearchBarUM, modifier: Modifier = Modifier) { val background = LocalMainBottomSheetColor.current.value + SearchBar( + modifier = modifier + .drawBehind { drawRect(background) } + .padding(horizontal = 16.dp) + .padding(bottom = 12.dp), + state = searchBarUM, + colors = TangemSearchBarDefaults.defaultTextFieldColors.copy( + focusedContainerColor = TangemTheme.colors.field.focused, + unfocusedContainerColor = TangemTheme.colors.field.focused, + ), + ) +} + +@Composable +internal fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) { + val background = LocalMainBottomSheetColor.current.value + Column( modifier = modifier .fillMaxSize() .verticalScroll(rememberScrollState()) .drawBehind { drawRect(background) }, ) { - SearchBar( - modifier = Modifier - .drawBehind { drawRect(background) } - .padding( - start = 16.dp, - end = 16.dp, - bottom = 8.dp, - ) - .onGloballyPositioned { coordinates -> - if (coordinates.size.height > 0) { - with(density) { - onHeaderSizeChange(coordinates.size.height.toDp()) - } - } - } - .padding(bottom = 4.dp), - state = state.searchBar, - ) - SpacerH(20.dp) Text( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .padding(horizontal = 20.dp), text = stringResourceSafe(R.string.feed_market_and_news), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, @@ -100,7 +93,7 @@ internal fun FeedList(state: FeedListUM, onHeaderSizeChange: (Dp) -> Unit, modif Text( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .padding(horizontal = 20.dp), text = state.currentDate, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.tertiary, @@ -156,6 +149,17 @@ private fun MarketBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: @Composable private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) { if (marketChartConfig.marketCharts.isNotEmpty()) { + Header( + title = { + Text( + text = stringResourceSafe(R.string.markets_common_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + }, + onSeeAllClick = { feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) }, + ) + LazyRow( modifier = Modifier.padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, @@ -175,17 +179,6 @@ private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallb } } - Header( - title = { - Text( - text = stringResourceSafe(R.string.markets_common_title), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - ) - }, - onSeeAllClick = { feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) }, - ) - SpacerH(12.dp) AnimatedContent( @@ -207,18 +200,38 @@ private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallb @Suppress("CanBeNonNullable") @Composable -private fun NewsBlock( +private fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) { + AnimatedContent(news) { newsUM -> + when (newsUM) { + is NewsUM.Content -> { + if (newsUM.content.isNotEmpty()) { + NewsContentBlock( + feedListCallbacks = feedListCallbacks, + news = newsUM, + trendingArticle = trendingArticle, + ) + } + } + NewsUM.Loading -> { + NewsLoadingBlock() + } + } + } +} + +@Composable +private fun NewsContentBlock( feedListCallbacks: FeedListCallbacks, - news: ImmutableList, + news: NewsUM.Content, trendingArticle: ArticleConfigUM?, ) { - if (news.isNotEmpty()) { + Column { Header( title = { Row(verticalAlignment = Alignment.CenterVertically) { Text( text = stringResourceSafe(R.string.common_news), - style = TangemTheme.typography.subtitle1, + style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, ) @@ -244,13 +257,12 @@ private fun NewsBlock( append(stringResourceSafe(R.string.feed_tangem_ai)) } }, - style = TangemTheme.typography.subtitle1, + style = TangemTheme.typography.h3, ) } }, onSeeAllClick = feedListCallbacks.onOpenAllNews, ) - SpacerH(12.dp) trendingArticle?.let { article -> @@ -260,8 +272,8 @@ private fun NewsBlock( .padding(horizontal = 16.dp), articleConfigUM = article, onArticleClick = { feedListCallbacks.onArticleClick(article.id) }, + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), ) - SpacerH(12.dp) } @@ -272,13 +284,16 @@ private fun NewsBlock( state = rememberLazyListState(), ) { items( - items = news, + items = news.content, key = ArticleConfigUM::id, ) { article -> ArticleCard( articleConfigUM = article, onArticleClick = { feedListCallbacks.onArticleClick(article.id) }, - modifier = Modifier.size(164.dp), + modifier = Modifier + .height(164.dp) + .widthIn(max = 216.dp), + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), ) } } @@ -292,6 +307,7 @@ private fun Header(title: @Composable () -> Unit, onSeeAllClick: () -> Unit) { .fillMaxWidth() .padding(horizontal = 20.dp), horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, ) { title() @@ -310,7 +326,10 @@ private fun Charts( onItemClick: (MarketsListItemUM) -> Unit, modifier: Modifier = Modifier, ) { - BlockCard(modifier) { + BlockCard( + modifier = modifier, + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) { Column( modifier = Modifier .fillMaxWidth() @@ -380,9 +399,6 @@ private val LinearGradientSecondPart = Color(0xFFE05AED) @Composable private fun FeedListPreview() { TangemThemePreview { - FeedList( - state = createFeedPreviewState(), - onHeaderSizeChange = {}, - ) + FeedListContent(state = createFeedPreviewState()) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt new file mode 100644 index 0000000000..69f94e61e6 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt @@ -0,0 +1,100 @@ +package com.tangem.features.feed.ui.feed + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +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.common.ui.news.DefaultLoadingArticle +import com.tangem.common.ui.news.TrendingLoadingArticle +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.res.TangemThemePreview +import com.tangem.features.feed.ui.market.components.MarketsListItemPlaceholder + +@Composable +internal fun MarketLoadingBlock() { + RectangleShimmer( + modifier = Modifier + .padding(start = 16.dp) + .size(width = 104.dp, height = 18.dp), + ) + SpacerH(12.dp) + ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp)) + SpacerH(32.dp) +} + +@Composable +internal fun MarketPulseLoadingBlock() { + RectangleShimmer() + SpacerH(8.dp) + LazyRow( + modifier = Modifier.padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + state = rememberLazyListState(), + ) { + items(DEFAULT_CHART_SIZE_IN_MARKET) { + RectangleShimmer(modifier = Modifier.size(width = 124.dp, height = 36.dp)) + } + } + SpacerH(12.dp) + ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp)) + SpacerH(32.dp) +} + +@Composable +internal fun NewsLoadingBlock() { + Column { + RectangleShimmer() + SpacerH(12.dp) + TrendingLoadingArticle(modifier = Modifier.padding(horizontal = 16.dp)) + SpacerH(12.dp) + LazyRow( + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + state = rememberLazyListState(), + ) { + items(DEFAULT_CHART_SIZE_IN_MARKET) { + DefaultLoadingArticle() + } + } + } +} + +@Composable +private fun ChartsLoading(modifier: Modifier = Modifier) { + BlockCard(modifier) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + repeat(DEFAULT_CHART_SIZE_IN_MARKET) { + MarketsListItemPlaceholder() + } + } + } +} + +private const val DEFAULT_CHART_SIZE_IN_MARKET = 5 + +@Preview(showBackground = true) +@Composable +private fun FeedListLoadingPreview() { + TangemThemePreview { + Column { + NewsLoadingBlock() + SpacerH(10.dp) + MarketLoadingBlock() + SpacerH(10.dp) + MarketPulseLoadingBlock() + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt index 734644fa3d..747d3d49bc 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt @@ -1,11 +1,11 @@ package com.tangem.features.feed.ui.feed.preview import com.tangem.common.ui.charts.state.MarketChartRawData -import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.common.ui.news.ArticleConfigUM -import com.tangem.common.ui.news.ArticleTagUM -import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference @@ -38,7 +38,7 @@ internal object FeedListPreviewDataProvider { onMarketItemClick = {}, onSortTypeClick = {}, ), - news = articles.filter { it.isTrending.not() }.toImmutableList(), + news = NewsUM.Content(articles.filter { it.isTrending.not() }.toImmutableList()), trendingArticle = articles.first { it.isTrending }, marketChartConfig = MarketChartConfig( marketCharts = createMarketCharts(marketItems, includeErrorState = false), @@ -159,21 +159,18 @@ internal object FeedListPreviewDataProvider { ), ) - private fun createArticleTags(): ImmutableSet { + private fun createArticleTags(): ImmutableSet { return persistentSetOf( - ArticleTagUM.Token( - title = TextReference.Str("BTC"), - iconState = CurrencyIconState.CoinIcon( - url = "", - fallbackResId = 0, - isGrayscale = false, - shouldShowCustomBadge = false, + LabelUM( + text = TextReference.Str("BTC"), + leadingContent = LabelLeadingContentUM.Token( + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/euro-coin.png", ), ), - ArticleTagUM.Category(TextReference.Str("Regulation")), - ArticleTagUM.Category(TextReference.Str("BTC")), - ArticleTagUM.Category(TextReference.Str("Supply")), - ArticleTagUM.Category(TextReference.Str("Demand")), + LabelUM(TextReference.Str("Regulation")), + LabelUM(TextReference.Str("BTC")), + LabelUM(TextReference.Str("Supply")), + LabelUM(TextReference.Str("Demand")), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt index 7e002be1f4..60f842ea9b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt @@ -14,7 +14,7 @@ internal data class FeedListUM( val currentDate: String, val searchBar: SearchBarUM, val feedListCallbacks: FeedListCallbacks, - val news: ImmutableList, + val news: NewsUM, val trendingArticle: ArticleConfigUM?, val marketChartConfig: MarketChartConfig, ) @@ -28,6 +28,12 @@ internal data class FeedListCallbacks( val onSortTypeClick: (SortByTypeUM) -> Unit, ) +@Immutable +internal sealed interface NewsUM { + data object Loading : NewsUM + data class Content(val content: ImmutableList) : NewsUM +} + internal data class MarketChartConfig( val marketCharts: ImmutableMap, val currentSortByType: SortByTypeUM = SortByTypeUM.TopGainers, @@ -49,7 +55,7 @@ internal sealed interface MarketChartUM { data class LoadingError(val onRetryClicked: () -> Unit) : MarketChartUM } -data class SortChartConfigUM( +internal data class SortChartConfigUM( val sortByType: SortByTypeUM, val isSelected: Boolean, ) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/SearchBarStateFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/SearchBarStateFactory.kt new file mode 100644 index 0000000000..bd33df97cc --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/SearchBarStateFactory.kt @@ -0,0 +1,24 @@ +package com.tangem.features.feed.ui.feed.state + +import com.tangem.utils.Provider + +internal class SearchBarStateFactory( + private val currentStateProvider: Provider, + private val onStateUpdate: (FeedListUM) -> Unit, +) { + + val searchQuery: String + get() = currentStateProvider().searchBar.query + + fun onSearchQueryChange(query: String) { + val currentState = currentStateProvider() + onStateUpdate( + currentState.copy( + searchBar = currentState.searchBar.copy( + query = query, + isActive = query.isNotEmpty(), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt new file mode 100644 index 0000000000..52bc5e2c2f --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt @@ -0,0 +1,83 @@ +package com.tangem.features.feed.ui.feed.state + +import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.news.ShortArticle +import com.tangem.utils.Provider +import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toPersistentSet + +internal class TrendingNewsStateFactory( + private val currentStateProvider: Provider, + private val onStateUpdate: (FeedListUM) -> Unit, +) { + + fun updateTrendingNewsState(news: List) { + val trendingArticleIndex = news.indexOfFirst { it.isTrending } + val trendingArticle = if (trendingArticleIndex != -1) news[trendingArticleIndex] else null + val commonArticles = if (trendingArticleIndex != -1) { + news.toMutableList().apply { removeAt(trendingArticleIndex) } + } else { + news + } + val currentState = currentStateProvider() + onStateUpdate( + currentState.copy( + trendingArticle = trendingArticle?.let { article -> + ArticleConfigUM( + id = article.id, + title = article.title, + score = article.score, + isTrending = true, + tags = article.categories.map { category -> + LabelUM(text = TextReference.Str(category.name)) + }.plus( + article.relatedTokens.map { token -> + LabelUM( + text = TextReference.Str(token.symbol), + leadingContent = LabelLeadingContentUM.Token( + iconUrl = getTokenIconUrlFromDefaultHost( + tokenId = CryptoCurrency.RawID(token.id), + ), + ), + ) + }, + ).toPersistentSet(), + createdAt = "1 min ago", // TODO in [REDACTED_TASK_KEY] + isViewed = article.viewed, + ) + }, + news = NewsUM.Content( + commonArticles.map { article -> + ArticleConfigUM( + id = article.id, + title = article.title, + score = article.score, + isTrending = false, + tags = article.categories.map { category -> + LabelUM(text = TextReference.Str(category.name)) + }.plus( + article.relatedTokens.map { token -> + LabelUM( + text = TextReference.Str(token.symbol), + leadingContent = LabelLeadingContentUM.Token( + iconUrl = getTokenIconUrlFromDefaultHost( + tokenId = CryptoCurrency.RawID(token.id), + ), + ), + ) + }, + ).toPersistentSet(), + createdAt = "1 min ago", // TODO in [REDACTED_TASK_KEY] + isViewed = article.viewed, + ) + }.toPersistentList(), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/home/impl/detekt-baseline-debug.xml b/features/home/impl/detekt-baseline-debug.xml index 684e103473..55111bd85c 100644 --- a/features/home/impl/detekt-baseline-debug.xml +++ b/features/home/impl/detekt-baseline-debug.xml @@ -6,9 +6,6 @@ BooleanPropertyNaming:HomeUM.kt$HomeUM$val scanInProgress: Boolean MultilineLambdaItParameter:HomeModel.kt$HomeModel${ delay(HIDE_PROGRESS_DELAY) setLoading(false) when (it) { is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet) } } MultilineLambdaItParameter:StoriesProgressBar.kt${ when (index) { currentStep -> it.fillMaxWidth(progress.value) in 0 until currentStep -> it.fillMaxWidth(fraction = 1f) else -> it } } - NonBooleanPropertyPrefixedWithIs:StoriesAnimation.kt$val isFirstStepLaunched = remember { mutableStateOf(false) } - NonBooleanPropertyPrefixedWithIs:StoriesAnimation.kt$val isLaunched = remember { mutableStateOf(false) } - NonBooleanPropertyPrefixedWithIs:StoriesAnimation.kt$val isSecondStepLaunched = remember { mutableStateOf(false) } ReusedModifierInstance:HomeButtonsV2.kt$StoriesButton( modifier = modifier, text = stringResourceSafe(id = R.string.common_get_started), useDarkerColors = false, onClick = onGetStartedClick, ) diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt index f82b96124a..1dbc4c7a1f 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt @@ -8,8 +8,8 @@ import com.tangem.common.routing.AppRouter import com.tangem.common.routing.entity.InitScreenLaunchMode import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic.SignedIn -import com.tangem.core.analytics.models.Basic.SignedIn.SignInType +import com.tangem.core.analytics.models.Basic.SignedInLegacy +import com.tangem.core.analytics.models.Basic.SignedInLegacy.SignInType import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -93,7 +93,7 @@ internal class HomeModel @Inject constructor( val uiState = _uiState.asStateFlow() init { - analyticsEventHandler.send(IntroductionProcess.ScreenOpened) + analyticsEventHandler.send(IntroductionProcess.ScreenOpened()) observeUserCountryChanges() when (params.launchMode) { @@ -127,20 +127,20 @@ internal class HomeModel @Inject constructor( } private fun onScanClick() { - analyticsEventHandler.send(IntroductionProcess.ButtonScanCard) + analyticsEventHandler.send(IntroductionProcess.ButtonScanCardLegacy()) scanCard() } private fun onShopClick() { - analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards) - analyticsEventHandler.send(Shop.ScreenOpened) + analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards()) + analyticsEventHandler.send(Shop.ScreenOpened()) modelScope.launch { generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } } } private fun onSearchTokensClick() { - analyticsEventHandler.send(IntroductionProcess.ButtonTokensList) + analyticsEventHandler.send(IntroductionProcess.ButtonTokensList()) router.push(AppRoute.ManageTokens(Source.STORIES)) } @@ -212,7 +212,7 @@ internal class HomeModel @Inject constructor( val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) if (currency != null) { analyticsEventHandler.send( - SignedIn( + SignedInLegacy( currency = currency, batch = scanResponse.card.batchId, signInType = SignInType.Card, diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateMobileWalletComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateMobileWalletComponent.kt index e9bdbf64e5..72db10452d 100644 --- a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateMobileWalletComponent.kt +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateMobileWalletComponent.kt @@ -4,5 +4,9 @@ import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent interface CreateMobileWalletComponent : ComposableContentComponent { - interface Factory : ComponentFactory + data class Params( + val source: String, + ) + + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/hot-wallet/impl/detekt-baseline-debug.xml b/features/hot-wallet/impl/detekt-baseline-debug.xml index 897f428e06..2e3bf90a57 100644 --- a/features/hot-wallet/impl/detekt-baseline-debug.xml +++ b/features/hot-wallet/impl/detekt-baseline-debug.xml @@ -11,17 +11,13 @@ BooleanPropertyNaming:ForgetWalletUM.kt$ForgetWalletUM$val secondCheckboxChecked: Boolean BooleanPropertyNaming:HotAccessCodeRequestUM.kt$HotAccessCodeRequestUM$val useBiometricVisible: Boolean = true BooleanPropertyNaming:HotWalletStepperComponent.kt$HotWalletStepperComponent.StepperUM$val showBackButton: Boolean - BooleanPropertyNaming:HotWalletStepperComponent.kt$HotWalletStepperComponent.StepperUM$val showFeedbackButton: Boolean BooleanPropertyNaming:HotWalletStepperComponent.kt$HotWalletStepperComponent.StepperUM$val showSkipButton: Boolean BooleanPropertyNaming:ManualBackupCheckUM.kt$ManualBackupCheckUM$val completeButtonEnabled: Boolean BooleanPropertyNaming:ManualBackupCheckUM.kt$ManualBackupCheckUM$val completeButtonProgress: Boolean BooleanPropertyNaming:ManualBackupCheckUM.kt$ManualBackupCheckUM.WordField$val error: Boolean BooleanPropertyNaming:MobileWalletSetupFinishedContent.kt$var showConfetti by remember { mutableStateOf(false) } - BooleanPropertyNaming:UpgradeWalletModel.kt$UpgradeWalletModel$val otherWalletAndAlreadyCreated by lazy { userWallet?.walletId != params.userWalletId && it.card.wallets.map { it.curve }.toSet().isNotEmpty() } - BooleanPropertyNaming:UpgradeWalletModel.kt$UpgradeWalletModel$val sameWalletButNotFinishedBackup by lazy { userWallet?.walletId == params.userWalletId && BackupValidator.isValidFull(it.card).not() } BooleanPropertyNaming:WalletBackupUM.kt$WalletBackupUM$val backedUp: Boolean BooleanPropertyNaming:WalletHardwareBackupUM.kt$WalletHardwareBackupUM$val showPurchaseBlock: Boolean = false - MaxChainedCallsOnSameLine:UpgradeWalletModel.kt$UpgradeWalletModel$it.card.wallets.map { it.curve }.toSet().isNotEmpty() MultilineLambdaItParameter:AddExistingWalletImportModel.kt$AddExistingWalletImportModel${ Timber.e(it) setImportProgress(false) } MultilineLambdaItParameter:AddExistingWalletImportModel.kt$AddExistingWalletImportModel${ setImportProgress(false) when (it) { is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -> { uiMessageSender.send( SnackbarMessage(resourceReference(R.string.hw_import_seed_phrase_already_imported)), ) } } } MultilineLambdaItParameter:CreateHardwareWalletModel.kt$CreateHardwareWalletModel${ delay(HIDE_PROGRESS_DELAY) setLoading(false) when (it) { is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -> { userWalletsListRepository.unlock( userWalletId = userWallet.walletId, unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), ).onRight { router.replaceAll(AppRoute.Wallet) } } } } @@ -48,14 +44,12 @@ MultilineLambdaItParameter:ManualBackupCheckModel.kt$ManualBackupCheckModel${ it.copy( words = seedPhrasePrivateInfo.mnemonic.mnemonicComponents.filterIndexed { index, _ -> WORD_FIELD_INDICES.contains(index + 1) }.toImmutableList(), ) } MultilineLambdaItParameter:ManualBackupPhraseContent.kt${ EnumeratedTwoColumnGridItem( index = it + 1, mnemonic = "word${it + 1}", ) } MultilineLambdaItParameter:ManualBackupPhraseModel.kt$ManualBackupPhraseModel${ it.copy( words = seedPhrasePrivateInfo.mnemonic.mnemonicComponents.mapIndexed { index, s -> EnumeratedTwoColumnGridItem(index + 1, s) }.toImmutableList(), ) } - MultilineLambdaItParameter:UpgradeWalletModel.kt$UpgradeWalletModel${ // Check if user attempted to upgrade before but something went wrong and a full reset is required val userWallet = coldUserWalletBuilderFactory.create(it).build() val sameWalletButNotFinishedBackup by lazy { userWallet?.walletId == params.userWalletId && BackupValidator.isValidFull(it.card).not() } val otherWalletAndAlreadyCreated by lazy { userWallet?.walletId != params.userWalletId && it.card.wallets.map { it.curve }.toSet().isNotEmpty() } if (userWallet != null && (sameWalletButNotFinishedBackup || otherWalletAndAlreadyCreated)) { startResetCardsFlow.emit(userWallet) return@doOnSuccess } delay(DELAY_SDK_DIALOG_CLOSE) tangemSdkManager.changeDisplayedCardIdNumbersCount(it) navigateToUpgradeFlow(it) } MultilineLambdaItParameter:ViewPhraseContent.kt${ EnumeratedTwoColumnGridItem( index = it + 1, mnemonic = "word${it + 1}", ) } MultilineLambdaItParameter:ViewPhraseModel.kt$ViewPhraseModel${ it.copy( words = words.mapIndexed { index, s -> EnumeratedTwoColumnGridItem(index + 1, s) }.toImmutableList(), ) } NoNameShadowing:ManualBackupCheckModel.kt$ManualBackupCheckModel${ it.copy(completeButtonProgress = false) } PropertyUsedBeforeDeclaration:AddExistingWalletImportModel.kt$AddExistingWalletImportModel$uiState ReusedModifierInstance:AddExistingWalletImportContent.kt$OutlineTextFieldWithIcon( modifier = modifier .padding(horizontal = 16.dp) .fillMaxWidth(), value = state.passPhrase, onValueChange = state.passPhraseChange, iconResId = R.drawable.ic_information_24, iconColor = TangemTheme.colors.icon.informative, label = stringResourceSafe(id = R.string.common_passphrase), placeholder = stringResourceSafe(id = R.string.send_optional_field), onIconClick = state.onPassphraseInfoClick, keyboardOptions = KeyboardOptions( autoCorrectEnabled = false, keyboardType = KeyboardType.Password, ), ) ReusedModifierInstance:HotAccessCodeRequestFullScreenContent.kt$AnimatedVisibility( modifier = modifier, visible = state.isShown, enter = fadeIn(), exit = fadeOut(), ) { Column( Modifier .fillMaxSize() .background(TangemTheme.colors.background.primary), horizontalAlignment = Alignment.CenterHorizontally, ) { TangemTopAppBar( modifier = Modifier.statusBarsPadding(), startButton = TopAppBarButtonUM.Back(state.onDismiss), ) SpacerH(68.dp) Column( Modifier .weight(1f) .fillMaxWidth() .padding(horizontal = 24.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Text( modifier = Modifier.animateEnterExit( enter = slideInVertically( tween(), initialOffsetY = { it + 200 }, ) + fadeIn(tween()), exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()), ), text = stringResourceSafe(R.string.access_code_check_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) SpacerH24() PinTextField( modifier = Modifier.animateEnterExit( enter = slideInVertically( tween(), initialOffsetY = { it + 200 }, ) + fadeIn(tween()), exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()), ), length = 6, isPasswordVisual = true, value = state.accessCode, pinTextColor = state.accessCodeColor, onValueChange = state.onAccessCodeChange, ) SpacerH(20.dp) AnimatedVisibility( modifier = Modifier.animateEnterExit( enter = slideInVertically( tween(), initialOffsetY = { it + 200 }, ) + fadeIn(tween()), exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()), ), visible = state.wrongAccessCodeText != null, enter = fadeIn(), exit = fadeOut(), ) { val wrongAccessCodeText = state.wrongAccessCodeText ?: return@AnimatedVisibility Text( text = wrongAccessCodeText.resolveReference(), textAlign = TextAlign.Center, style = TangemTheme.typography.caption2.copy( lineBreak = LineBreak.Heading, ), color = TangemTheme.colors.text.warning, ) } } AnimatedVisibility( visible = state.useBiometricVisible, enter = fadeIn(), exit = fadeOut(), ) { SecondaryButton( modifier = Modifier .padding(16.dp) .fillMaxWidth() .navigationBarsPadding() .imePadding(), text = stringResourceSafe( id = R.string.welcome_unlock, stringResourceSafe(R.string.common_biometrics), ), onClick = state.useBiometricClick, ) } } } - ReusedModifierInstance:HotWalletStepper.kt$TangemTopAppBar( startButton = if (state.showBackButton) { TopAppBarButtonUM.Back(onBackClick) } else { null }, endButton = when { state.showSkipButton -> TopAppBarButtonUM.Text( text = resourceReference(R.string.common_skip), onClicked = onSkipClick, ) state.showFeedbackButton -> TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_chat_24, onClicked = onFeedbackClick, ) else -> null }, title = state.title, containerColor = TangemTheme.colors.background.primary, modifier = modifier, titleAlignment = Alignment.CenterHorizontally, ) SuspendFunSwallowedCancellation:AddExistingWalletImportModel.kt$AddExistingWalletImportModel$runCatching SuspendFunSwallowedCancellation:ManualBackupCheckModel.kt$ManualBackupCheckModel$runCatching SuspendFunSwallowedCancellation:ManualBackupPhraseModel.kt$ManualBackupPhraseModel$runCatching diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt index 318bce1dc7..595dff3477 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt @@ -11,6 +11,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.hotwallet.IsAccessCodeSimpleUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.settings.CanUseBiometryUseCase @@ -28,7 +29,6 @@ import com.tangem.hot.sdk.model.HotAuth import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -57,6 +57,7 @@ internal class AccessCodeModel @Inject constructor( private val shouldShowAskBiometryUseCase: ShouldShowAskBiometryUseCase, private val setAskBiometryShownUseCase: SetAskBiometryShownUseCase, private val canUseBiometryUseCase: CanUseBiometryUseCase, + private val isAccessCodeSimpleUseCase: IsAccessCodeSimpleUseCase, private val uiMessageSender: UiMessageSender, ) : Model() { @@ -118,16 +119,45 @@ internal class AccessCodeModel @Inject constructor( modelScope.launch { delay(timeMillis = SUCCESS_DISPLAY_DURATION_MS) - params.callbacks.onNewAccessCodeInput(params.userWalletId, uiState.value.accessCode) - - uiState.update { currentState -> - currentState.copy( - accessCode = "", - ) + if (isAccessCodeSimpleUseCase(uiState.value.accessCode)) { + showSimpleAccessCodeDialog() + } else { + setNewCode() } } } + private fun setNewCode() { + params.callbacks.onNewAccessCodeInput(params.userWalletId, uiState.value.accessCode) + + uiState.update { currentState -> + currentState.copy( + accessCode = "", + ) + } + } + + private fun showSimpleAccessCodeDialog() { + uiMessageSender.send( + DialogMessage( + title = resourceReference(R.string.access_code_alert_validation_title), + message = resourceReference(R.string.access_code_alert_validation_description), + firstAction = EventMessageAction( + title = resourceReference(R.string.access_code_alert_validation_cancel), + onClick = { + uiState.update { currentState -> + currentState.copy(onAccessCodeChange = ::onAccessCodeChange) + } + }, + ), + secondAction = EventMessageAction( + title = resourceReference(R.string.access_code_alert_validation_ok), + onClick = ::setNewCode, + ), + ), + ) + } + private suspend fun showErrorAndReset() { uiState.update { currentState -> currentState.copy( @@ -152,11 +182,6 @@ internal class AccessCodeModel @Inject constructor( tryToAskForBiometry() - userWalletsListRepository.saveWithoutLock( - userWallet.copy(backedUp = true), - canOverride = true, - ) - userWalletsListRepository.setLock( userWallet.walletId, UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()), @@ -179,30 +204,25 @@ internal class AccessCodeModel @Inject constructor( hotWalletAccessor.unlockContextual(userWallet.hotWalletId) } - launch(NonCancellable) { - var updatedHotWalletId = tangemHotSdk.changeAuth( + var updatedHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = unlockHotWallet, + auth = HotAuth.Password(accessCode.toCharArray()), + ) + + if (walletsRepository.requireAccessCode().not()) { + updatedHotWalletId = tangemHotSdk.changeAuth( unlockHotWallet = unlockHotWallet, - auth = HotAuth.Password(accessCode.toCharArray()), + auth = HotAuth.Biometry, ) - - if (walletsRepository.requireAccessCode().not()) { - updatedHotWalletId = tangemHotSdk.changeAuth( - unlockHotWallet = unlockHotWallet, - auth = HotAuth.Biometry, - ) - } - - userWalletsListRepository.saveWithoutLock( - userWallet.copy( - hotWalletId = updatedHotWalletId, - backedUp = true, - ), - canOverride = true, - ) - - clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId) } + userWalletsListRepository.saveWithoutLock( + userWallet.copy(hotWalletId = updatedHotWalletId), + canOverride = true, + ) + + clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId) + params.callbacks.onAccessCodeUpdated(params.userWalletId) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt index ad3e784dd7..ba2bbf512a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -1,5 +1,7 @@ package com.tangem.features.hotwallet.accesscoderequest +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.event.SignIn import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.ui.components.fields.PinTextColor @@ -33,6 +35,7 @@ internal class HotAccessCodeRequestModel @Inject constructor( private val hotAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, private val userWalletsListRepository: UserWalletsListRepository, private val canUseBiometryUseCase: CanUseBiometryUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val result = MutableStateFlow(null) @@ -108,6 +111,7 @@ internal class HotAccessCodeRequestModel @Inject constructor( onAccessCodeChange = ::onAccessCodeChange, accessCode = "", useBiometricClick = { + analyticsEventHandler.send(SignIn.ButtonBiometricSignIn()) dismissState() result.value = HotWalletPasswordRequester.Result.UseBiometry }, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt index e40072474a..51cfc12211 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt @@ -27,6 +27,7 @@ import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -110,7 +111,7 @@ internal class AddExistingWalletModel @Inject constructor( title = resourceReference(R.string.access_code_alert_skip_ok), onClick = { if (userWalletId != null) { - modelScope.launch { + modelScope.launch(NonCancellable) { setAccessCodeSkippedUseCase(userWalletId, true) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletStepperStateManager.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletStepperStateManager.kt index 587c85328d..0c2edb6dae 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletStepperStateManager.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletStepperStateManager.kt @@ -15,7 +15,6 @@ internal class AddExistingWalletStepperStateManager { title = resourceReference(R.string.wallet_import_seed_navtitle), showBackButton = true, showSkipButton = false, - showFeedbackButton = true, ) is AddExistingWalletRoute.BackupCompleted -> HotWalletStepperComponent.StepperUM( @@ -24,7 +23,6 @@ internal class AddExistingWalletStepperStateManager { title = resourceReference(R.string.wallet_import_title), showBackButton = false, showSkipButton = false, - showFeedbackButton = false, ) is AddExistingWalletRoute.SetAccessCode -> HotWalletStepperComponent.StepperUM( @@ -33,7 +31,6 @@ internal class AddExistingWalletStepperStateManager { title = resourceReference(R.string.access_code_navtitle), showBackButton = false, showSkipButton = true, - showFeedbackButton = false, ) is AddExistingWalletRoute.ConfirmAccessCode -> HotWalletStepperComponent.StepperUM( @@ -42,7 +39,6 @@ internal class AddExistingWalletStepperStateManager { title = resourceReference(R.string.access_code_navtitle), showBackButton = true, showSkipButton = true, - showFeedbackButton = false, ) is AddExistingWalletRoute.PushNotifications -> HotWalletStepperComponent.StepperUM( @@ -51,7 +47,6 @@ internal class AddExistingWalletStepperStateManager { title = resourceReference(R.string.onboarding_title_notifications), showBackButton = false, showSkipButton = false, - showFeedbackButton = false, ) is AddExistingWalletRoute.SetupFinished -> HotWalletStepperComponent.StepperUM( @@ -60,7 +55,6 @@ internal class AddExistingWalletStepperStateManager { title = resourceReference(R.string.common_done), showBackButton = false, showSkipButton = false, - showFeedbackButton = false, ) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index 7d8156ecd3..4245e9f79d 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -1,5 +1,8 @@ package com.tangem.features.hotwallet.addexistingwallet.im.port.model +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -37,6 +40,7 @@ internal class AddExistingWalletImportModel @Inject constructor( private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory, private val saveUserWalletUseCase: SaveWalletUseCase, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params: AddExistingWalletImportComponent.Params = paramsContainer.require() @@ -60,6 +64,7 @@ internal class AddExistingWalletImportModel @Inject constructor( } init { + analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ImportSeedPhraseScreenOpened()) importSeedPhraseUiStateBuilder = ImportSeedPhraseUiStateBuilder( modelScope = modelScope, mnemonicRepository = mnemonicRepository, @@ -72,6 +77,7 @@ internal class AddExistingWalletImportModel @Inject constructor( ) }, onPassphraseInfoClick = ::onPassphraseInfoClick, + onImportClick = { analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ButtonImport()) }, ) } @@ -101,6 +107,23 @@ internal class AddExistingWalletImportModel @Inject constructor( } .onRight { setImportProgress(false) + analyticsEventHandler.send( + event = OnboardingAnalyticsEvent.Onboarding.Finished( + source = AnalyticsParam.ScreensSources.ImportWallet.value, + ), + ) + analyticsEventHandler.send( + event = OnboardingAnalyticsEvent.CreateWallet.WalletCreatedSuccessfully( + source = AnalyticsParam.ScreensSources.ImportWallet.value, + creationType = OnboardingAnalyticsEvent.CreateWallet.WalletCreationType.SeedImport, + seedPhraseLength = mnemonic.mnemonicComponents.size, + passPhraseState = if (passphrase.isNullOrBlank()) { + AnalyticsParam.EmptyFull.Empty + } else { + AnalyticsParam.EmptyFull.Full + }, + ), + ) params.callbacks.onWalletImported(userWallet.walletId) } }.onFailure { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt index 95e7f9c2db..cfdb385983 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +@Suppress("LongParameterList") internal class ImportSeedPhraseUiStateBuilder( private val modelScope: CoroutineScope, private val mnemonicRepository: MnemonicRepository, @@ -24,6 +25,7 @@ internal class ImportSeedPhraseUiStateBuilder( private val updateUiState: ((AddExistingWalletImportUM) -> AddExistingWalletImportUM) -> Unit, private val importWallet: (mnemonic: Mnemonic, passphrase: String?) -> Unit, private val onPassphraseInfoClick: () -> Unit, + private val onImportClick: () -> Unit, ) { private val wordsCheckJobHolder = JobHolder() private var importedMnemonic: Mnemonic? = null @@ -57,6 +59,7 @@ internal class ImportSeedPhraseUiStateBuilder( } private fun onCreateWallet() { + onImportClick() val mnemonic = importedMnemonic ?: return val passphrase = passphrase?.takeIf { it.isNotEmpty() } importWallet(mnemonic, passphrase) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt index f76ab84370..6f7f584bfb 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt @@ -1,6 +1,7 @@ package com.tangem.features.hotwallet.common.ui import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -43,11 +44,12 @@ internal fun OptionBlock( } .padding(16.dp), ) { - Row { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { Text( modifier = Modifier - .weight(1f, fill = false) - .padding(end = 4.dp), + .weight(1f, fill = false), text = title, style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt index da08951a67..2d4b71b076 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt @@ -5,8 +5,8 @@ import com.tangem.common.core.TangemSdkError import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic.SignedIn -import com.tangem.core.analytics.models.Basic.SignedIn.SignInType +import com.tangem.core.analytics.models.Basic +import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router @@ -16,8 +16,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.card.ScanCardProcessor -import com.tangem.domain.card.analytics.ParamCardCurrencyConverter -import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.analytics.IntroductionProcess import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.error.SaveWalletError @@ -53,6 +52,7 @@ internal class CreateHardwareWalletModel @Inject constructor( private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, private val saveWalletUseCase: SaveWalletUseCase, private val userWalletsListRepository: UserWalletsListRepository, + private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { @@ -66,20 +66,26 @@ internal class CreateHardwareWalletModel @Inject constructor( ) init { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.CreateWalletScreenOpened) + trackingContextProxy.addHotWalletContext() + analyticsEventHandler.send(WalletSettingsAnalyticEvents.CreateWalletScreenOpened()) } override fun onDestroy() { + trackingContextProxy.removeContext() super.onDestroy() } private fun onBuyTangemWalletClick() { + analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.CreateWallet)) modelScope.launch { generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } } } private fun onScanDeviceClick() { + analyticsEventHandler.send( + event = IntroductionProcess.ButtonScanCard(AnalyticsParam.ScreensSources.CreateWallet), + ) scanCard() } @@ -143,28 +149,11 @@ internal class CreateHardwareWalletModel @Inject constructor( }, ifRight = { setLoading(false) - sendSignedInCardAnalyticsEvent(scanResponse = scanResponse, isImported = userWallet.isImported) router.replaceAll(AppRoute.Wallet) }, ) } - private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse, isImported: Boolean) { - val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) - if (currency != null) { - analyticsEventHandler.send( - SignedIn( - currency = currency, - batch = scanResponse.card.batchId, - signInType = SignInType.Card, - walletsCount = userWalletsListRepository.userWalletsSync().size.toString(), - isImported = isImported, - hasBackup = scanResponse.card.backupStatus?.isActive, - ), - ) - } - } - private fun setLoading(isLoading: Boolean) { uiState.update { it.copy(isScanInProgress = isLoading) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt index 47d9334760..0f188dfe43 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt @@ -1,13 +1,21 @@ package com.tangem.features.hotwallet.createmobilewallet import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog +import com.tangem.domain.hotwallet.IsHotWalletCreationSupported import com.tangem.domain.wallets.builder.HotUserWalletBuilder import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.domain.wallets.usecase.SyncWalletWithRemoteUseCase +import com.tangem.features.hotwallet.CreateMobileWalletComponent import com.tangem.features.hotwallet.createmobilewallet.entity.CreateMobileWalletUM import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.model.HotAuth @@ -25,6 +33,7 @@ import javax.inject.Inject @Suppress("LongParameterList") @ModelScoped internal class CreateMobileWalletModel @Inject constructor( + paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory, private val saveUserWalletUseCase: SaveWalletUseCase, @@ -32,8 +41,13 @@ internal class CreateMobileWalletModel @Inject constructor( private val router: Router, private val tangemHotSdk: TangemHotSdk, private val trackingContextProxy: TrackingContextProxy, + private val isHotWalletCreationSupported: IsHotWalletCreationSupported, + private val uiMessageSender: UiMessageSender, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { + private val params: CreateMobileWalletComponent.Params = paramsContainer.require() + internal val uiState: StateFlow field = MutableStateFlow( CreateMobileWalletUM( @@ -46,6 +60,12 @@ internal class CreateMobileWalletModel @Inject constructor( init { trackingContextProxy.addHotWalletContext() + analyticsEventHandler.send( + event = OnboardingAnalyticsEvent.Onboarding.Started(source = params.source), + ) + analyticsEventHandler.send( + event = OnboardingAnalyticsEvent.SeedPhrase.CreateMobileScreenOpened(source = params.source), + ) } override fun onDestroy() { @@ -54,10 +74,15 @@ internal class CreateMobileWalletModel @Inject constructor( } private fun onImportClick() { + analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ButtonImportWallet()) + checkHotWalletCreationSupported(notSupported = { return }) router.push(AppRoute.AddExistingWallet) } private fun onCreateClick() { + analyticsEventHandler.send(OnboardingAnalyticsEvent.CreateWallet.ButtonCreateWallet()) + checkHotWalletCreationSupported(notSupported = { return }) + modelScope.launch { uiState.update { it.copy(createButtonLoading = true) @@ -70,9 +95,20 @@ internal class CreateMobileWalletModel @Inject constructor( saveUserWalletUseCase(userWallet) - launch(NonCancellable) { + analyticsEventHandler.send(OnboardingAnalyticsEvent.Onboarding.Finished(source = params.source)) + analyticsEventHandler.send( + event = OnboardingAnalyticsEvent.CreateWallet.WalletCreatedSuccessfully( + source = params.source, + creationType = OnboardingAnalyticsEvent.CreateWallet.WalletCreationType.NewSeed, + seedPhraseLength = SEED_PHRASE_LENGTH, + passPhraseState = AnalyticsParam.EmptyFull.Empty, + ), + ) + + launch(dispatchers.main + NonCancellable) { syncWalletWithRemoteUseCase(userWalletId = userWallet.walletId) } + router.replaceAll(AppRoute.Wallet) }.onFailure { throwable -> Timber.e(throwable) @@ -81,4 +117,17 @@ internal class CreateMobileWalletModel @Inject constructor( } } } + + private inline fun checkHotWalletCreationSupported(notSupported: () -> Unit) { + if (!isHotWalletCreationSupported()) { + uiMessageSender.send( + hotWalletCreationNotSupportedDialog(isHotWalletCreationSupported.getLeastVersionName()), + ) + notSupported() + } + } + + companion object { + private const val SEED_PHRASE_LENGTH = 12 + } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/DefaultCreateMobileWalletComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/DefaultCreateMobileWalletComponent.kt index bf5fd61424..2dbd2f1aa4 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/DefaultCreateMobileWalletComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/DefaultCreateMobileWalletComponent.kt @@ -15,7 +15,7 @@ import dagger.assisted.AssistedInject @Suppress("UnusedPrivateMember") internal class DefaultCreateMobileWalletComponent @AssistedInject constructor( @Assisted private val context: AppComponentContext, - @Assisted private val params: Unit, + @Assisted private val params: CreateMobileWalletComponent.Params, ) : CreateMobileWalletComponent, AppComponentContext by context { private val model: CreateMobileWalletModel = getOrCreateModel(params) @@ -31,6 +31,9 @@ internal class DefaultCreateMobileWalletComponent @AssistedInject constructor( @AssistedFactory interface Factory : CreateMobileWalletComponent.Factory { - override fun create(context: AppComponentContext, params: Unit): DefaultCreateMobileWalletComponent + override fun create( + context: AppComponentContext, + params: CreateMobileWalletComponent.Params, + ): DefaultCreateMobileWalletComponent } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ui/ManualBackupStartContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ui/ManualBackupStartContent.kt index 56486d5ef7..fe37f62480 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ui/ManualBackupStartContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ui/ManualBackupStartContent.kt @@ -3,9 +3,12 @@ package com.tangem.features.hotwallet.manualbackup.start.ui import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -22,8 +25,8 @@ import com.tangem.features.hotwallet.manualbackup.start.entity.ManualBackupStart @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun ManualBackupStartContent(state: ManualBackupStartUM, modifier: Modifier = Modifier) { - Column( - modifier = modifier + Box( + modifier .background(TangemTheme.colors.background.primary) .fillMaxSize() .padding( @@ -33,53 +36,57 @@ internal fun ManualBackupStartContent(state: ManualBackupStartUM, modifier: Modi bottom = 16.dp, ), ) { - Text( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = 16.dp, - vertical = 8.dp, + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 16.dp, + vertical = 8.dp, + ), + text = stringResourceSafe(R.string.backup_info_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 16.dp, + vertical = 8.dp, + ), + text = stringResourceSafe( + R.string.backup_info_description, + state.seepPhraseLength.toString(), ), - text = stringResourceSafe(R.string.backup_info_title), - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ) - Text( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = 16.dp, - vertical = 8.dp, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + FeatureBlock( + modifier = Modifier + .padding(top = 24.dp), + title = stringResourceSafe(R.string.backup_info_save_title), + description = stringResourceSafe( + R.string.backup_info_save_description, + state.seepPhraseLength.toString(), ), - text = stringResourceSafe( - R.string.backup_info_description, - state.seepPhraseLength.toString(), - ), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - FeatureBlock( - modifier = Modifier - .padding(top = 24.dp), - title = stringResourceSafe(R.string.backup_info_save_title), - description = stringResourceSafe( - R.string.backup_info_save_description, - state.seepPhraseLength.toString(), - ), - iconRes = R.drawable.ic_lock_24, - ) - FeatureBlock( - modifier = Modifier - .padding(top = 24.dp), - title = stringResourceSafe(R.string.backup_info_keep_title), - description = stringResourceSafe(R.string.backup_info_keep_description), - iconRes = R.drawable.ic_settings_24, - ) - Spacer(modifier = Modifier.weight(1f)) + iconRes = R.drawable.ic_lock_24, + ) + FeatureBlock( + modifier = Modifier + .padding(top = 24.dp), + title = stringResourceSafe(R.string.backup_info_keep_title), + description = stringResourceSafe(R.string.backup_info_keep_description), + iconRes = R.drawable.ic_settings_24, + ) + Spacer(modifier = Modifier.weight(1f)) + } + PrimaryButton( modifier = Modifier + .align(Alignment.BottomCenter) .fillMaxWidth() .padding(top = 16.dp), text = stringResourceSafe(R.string.common_continue), diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt index 169d037699..4f3025382e 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt @@ -14,7 +14,6 @@ interface HotWalletStepperComponent : ComposableContentComponent { val title: TextReference, val showBackButton: Boolean, val showSkipButton: Boolean, - val showFeedbackButton: Boolean, ) { companion object { fun initialState() = StepperUM( @@ -23,7 +22,6 @@ interface HotWalletStepperComponent : ComposableContentComponent { title = TextReference.EMPTY, showBackButton = false, showSkipButton = false, - showFeedbackButton = false, ) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/DefaultHotWalletStepperComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/DefaultHotWalletStepperComponent.kt index 320920105b..9b79212aab 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/DefaultHotWalletStepperComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/DefaultHotWalletStepperComponent.kt @@ -34,7 +34,6 @@ internal class DefaultHotWalletStepperComponent @AssistedInject constructor( modifier = modifier, onBackClick = model::onBackClick, onSkipClick = model::onSkipClick, - onFeedbackClick = model::onFeedbackClick, ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt index 513718e705..5e26e5570d 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt @@ -34,9 +34,4 @@ internal class HotWalletStepperModel @Inject constructor( // TODO send analytics params.callback.onSkipClick() } - - fun onFeedbackClick() { - // TODO send analytics - // openFeedback() - } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt index ea029d37be..b6cd497ea4 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt @@ -31,7 +31,6 @@ internal fun HotWalletStepper( state: HotWalletStepperComponent.StepperUM, onBackClick: () -> Unit, onSkipClick: () -> Unit, - onFeedbackClick: () -> Unit, modifier: Modifier = Modifier, ) { val fraction = state.currentStep.toFloat() / state.steps.coerceAtLeast(1) @@ -47,20 +46,16 @@ internal fun HotWalletStepper( } else { null }, - endButton = when { - state.showSkipButton -> TopAppBarButtonUM.Text( + endButton = if (state.showSkipButton) { + TopAppBarButtonUM.Text( text = resourceReference(R.string.common_skip), onClicked = onSkipClick, ) - state.showFeedbackButton -> TopAppBarButtonUM.Icon( - iconRes = R.drawable.ic_chat_24, - onClicked = onFeedbackClick, - ) - else -> null + } else { + null }, title = state.title, containerColor = TangemTheme.colors.background.primary, - modifier = modifier, titleAlignment = Alignment.CenterHorizontally, ) @@ -95,11 +90,9 @@ private fun HotWalletStepper_Preview() { title = resourceReference(R.string.common_done), showBackButton = true, showSkipButton = false, - showFeedbackButton = true, ), onBackClick = {}, onSkipClick = {}, - onFeedbackClick = {}, ) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt index 3775021494..dd8b450432 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt @@ -28,6 +28,7 @@ internal class DefaultUpgradeWalletComponent @AssistedInject constructor( private val resetCardsComponent = resetCardsComponentFactory.create( context = child("ResetCardsComponent"), params = ResetCardsComponent.Params( + source = ResetCardsComponent.Params.Source.Upgrade, callbacks = model.resetCardsComponentCallbacks, ), ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt index 976804e17d..7b675b8238 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt @@ -7,6 +7,9 @@ import com.tangem.common.doOnResult import com.tangem.common.doOnSuccess import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic +import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -18,7 +21,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction -import com.tangem.domain.card.BackupValidator +import com.tangem.domain.card.analytics.IntroductionProcess import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.feedback.SendFeedbackEmailUseCase @@ -60,6 +63,7 @@ internal class UpgradeWalletModel @Inject constructor( private val tangemSdkManager: TangemSdkManager, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, + private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() @@ -77,22 +81,26 @@ internal class UpgradeWalletModel @Inject constructor( ) init { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.HardwareUpgradeScreenOpened) + trackingContextProxy.addHotWalletContext() + analyticsEventHandler.send(WalletSettingsAnalyticEvents.HardwareUpgradeScreenOpened()) } override fun onDestroy() { + trackingContextProxy.removeContext() clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId) super.onDestroy() } private fun onBuyTangemWalletClick() { + analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Upgrade)) modelScope.launch { generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } } } private fun onContinueClick() { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonStartUpgrade) + analyticsEventHandler.send(IntroductionProcess.ButtonScanCard(AnalyticsParam.ScreensSources.Upgrade)) + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonStartUpgrade()) scanCard() } @@ -131,12 +139,9 @@ internal class UpgradeWalletModel @Inject constructor( scanResponse: ScanResponse, onSuccess: suspend () -> Unit, ) { - // Check if user attempted to upgrade before but something went wrong and a full reset is required val userWallet = coldUserWalletBuilderFactory.create(scanResponse).build() - val isSameWalletButNotFinishedBackup = userWallet?.walletId == params.userWalletId && - BackupValidator.isValidFull(scanResponse.card).not() - if (userWallet != null && isSameWalletButNotFinishedBackup) { + if (userWallet?.walletId == params.userWalletId) { startResetCardsFlow.emit(userWallet) return } @@ -201,7 +206,7 @@ internal class UpgradeWalletModel @Inject constructor( } override fun onComplete() { - setLoading(false) + scanCard() } } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt index d8f16f65e8..3f91e6d246 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt @@ -33,6 +33,7 @@ import com.tangem.features.hotwallet.WalletActivationComponent import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -70,16 +71,16 @@ internal class WalletActivationModel @Inject constructor( } val currentRoute: MutableStateFlow = MutableStateFlow(startRoute) - private val source = AnalyticsParam.ScreensSources.Main - private val action = WalletSettingsAnalyticEvents.RecoveryPhraseScreenAction.Backup + private val analyticsSource = AnalyticsParam.ScreensSources.Main + private val analyticsAction = WalletSettingsAnalyticEvents.RecoveryPhraseScreenAction.Backup init { trackingContextProxy.addHotWalletContext() if (startRoute is WalletActivationRoute.ManualBackupStart) { analyticsEventHandler.send( event = WalletSettingsAnalyticEvents.RecoveryPhraseScreenInfo( - source = source.value, - action = action.value, + source = analyticsSource.value, + action = analyticsAction.value, ), ) } @@ -134,7 +135,7 @@ internal class WalletActivationModel @Inject constructor( secondAction = EventMessageAction( title = resourceReference(R.string.access_code_alert_skip_ok), onClick = { - modelScope.launch { + modelScope.launch(NonCancellable) { setAccessCodeSkippedUseCase(userWalletId, true) } navigateToPushNotificationsOrNext() @@ -160,8 +161,8 @@ internal class WalletActivationModel @Inject constructor( stackNavigation.push(WalletActivationRoute.ManualBackupPhrase) analyticsEventHandler.send( event = WalletSettingsAnalyticEvents.RecoveryPhraseScreen( - source = source.value, - action = action.value, + source = analyticsSource.value, + action = analyticsAction.value, ), ) } @@ -172,8 +173,8 @@ internal class WalletActivationModel @Inject constructor( stackNavigation.push(WalletActivationRoute.ManualBackupCheck) analyticsEventHandler.send( event = WalletSettingsAnalyticEvents.RecoveryPhraseCheck( - source = source.value, - action = action.value, + source = analyticsSource.value, + action = analyticsAction.value, ), ) } @@ -184,8 +185,8 @@ internal class WalletActivationModel @Inject constructor( stackNavigation.push(WalletActivationRoute.ManualBackupCompleted) analyticsEventHandler.send( event = WalletSettingsAnalyticEvents.BackupCompleteScreen( - source = source.value, - action = action.value, + source = analyticsSource.value, + action = analyticsAction.value, ), ) } @@ -193,6 +194,9 @@ internal class WalletActivationModel @Inject constructor( inner class ManualBackupCompletedModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks { override fun onContinueClick(userWalletId: UserWalletId) { + analyticsEventHandler.send( + event = WalletSettingsAnalyticEvents.AccessCodeScreenOpened(source = analyticsSource.value), + ) stackNavigation.push(WalletActivationRoute.SetAccessCode) } @@ -201,6 +205,9 @@ internal class WalletActivationModel @Inject constructor( inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks { override fun onNewAccessCodeInput(userWalletId: UserWalletId, accessCode: String) { + analyticsEventHandler.send( + event = WalletSettingsAnalyticEvents.ReEnterAccessCodeScreen(source = analyticsSource.value), + ) stackNavigation.push(WalletActivationRoute.ConfirmAccessCode(accessCode)) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationStepperStateManager.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationStepperStateManager.kt index 7c3056c55c..22ae7f9af8 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationStepperStateManager.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationStepperStateManager.kt @@ -16,7 +16,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() { title = resourceReference(R.string.common_backup), showBackButton = true, showSkipButton = false, - showFeedbackButton = true, ) is WalletActivationRoute.ManualBackupPhrase -> HotWalletStepperComponent.StepperUM( currentStep = STEP_BACKUP_PHRASE, @@ -24,7 +23,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() { title = resourceReference(R.string.common_backup), showBackButton = true, showSkipButton = false, - showFeedbackButton = true, ) is WalletActivationRoute.ManualBackupCheck -> HotWalletStepperComponent.StepperUM( currentStep = STEP_BACKUP_CHECK, @@ -32,7 +30,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() { title = resourceReference(R.string.common_backup), showBackButton = true, showSkipButton = false, - showFeedbackButton = true, ) is WalletActivationRoute.ManualBackupCompleted -> HotWalletStepperComponent.StepperUM( currentStep = STEP_BACKUP_COMPLETED, @@ -40,7 +37,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() { title = resourceReference(R.string.common_backup), showBackButton = false, showSkipButton = false, - showFeedbackButton = false, ) is WalletActivationRoute.SetAccessCode -> HotWalletStepperComponent.StepperUM( currentStep = STEP_ACCESS_CODE, @@ -48,7 +44,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() { title = resourceReference(R.string.access_code_navtitle), showBackButton = false, showSkipButton = true, - showFeedbackButton = false, ) is WalletActivationRoute.ConfirmAccessCode -> HotWalletStepperComponent.StepperUM( currentStep = STEP_ACCESS_CODE, @@ -56,7 +51,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() { title = resourceReference(R.string.access_code_navtitle), showBackButton = true, showSkipButton = true, - showFeedbackButton = false, ) is WalletActivationRoute.PushNotifications -> HotWalletStepperComponent.StepperUM( currentStep = STEP_NOTIFICATIONS, @@ -64,7 +58,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() { title = resourceReference(R.string.onboarding_title_notifications), showBackButton = false, showSkipButton = false, - showFeedbackButton = false, ) is WalletActivationRoute.SetupFinished -> HotWalletStepperComponent.StepperUM( currentStep = STEP_DONE, @@ -72,7 +65,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() { title = resourceReference(R.string.common_done), showBackButton = false, showSkipButton = false, - showFeedbackButton = false, ) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt index 1e183bf849..f1a729835a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt @@ -3,6 +3,8 @@ package com.tangem.features.hotwallet.walletbackup.model import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic +import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -24,6 +26,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @Suppress("LongParameterList") @@ -36,11 +39,14 @@ internal class WalletBackupModel @Inject constructor( private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, private val urlOpener: UrlOpener, private val router: Router, + private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params: WalletBackupComponent.Params = paramsContainer.require() + private val isScreenOpenedEventSent: AtomicBoolean = AtomicBoolean(false) + val uiState: StateFlow field = MutableStateFlow( WalletBackupUM( @@ -67,20 +73,33 @@ internal class WalletBackupModel @Inject constructor( ) init { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.BackupScreenOpened(isManualBackupEnabled = true)) + trackingContextProxy.addHotWalletContext() getUserWalletUseCase.invokeFlow(params.userWalletId) .onEach { either -> either.fold( ifLeft = { Timber.e("Error on getting user wallet: $it") }, - ifRight = { - updateBackupStatuses(it) + ifRight = { userWallet -> + if (!isScreenOpenedEventSent.get() && userWallet is UserWallet.Hot) { + analyticsEventHandler.send( + event = WalletSettingsAnalyticEvents.BackupScreenOpened( + isBackedUp = userWallet.backedUp, + ), + ) + isScreenOpenedEventSent.set(true) + } + updateBackupStatuses(userWallet) }, ) }.launchIn(modelScope) } + override fun onDestroy() { + trackingContextProxy.removeContext() + super.onDestroy() + } + private fun updateBackupStatuses(userWallet: UserWallet) { uiState.update { currentState -> if (userWallet is UserWallet.Hot) { @@ -111,13 +130,14 @@ internal class WalletBackupModel @Inject constructor( ) private fun onBuyClick() { + analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Backup)) modelScope.launch { generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } } } private fun onRecoveryPhraseClick() { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonRecoveryPhrase) + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonRecoveryPhrase()) if (uiState.value.backedUp) { getUserWalletUseCase.invoke(params.userWalletId) .fold( @@ -160,7 +180,7 @@ internal class WalletBackupModel @Inject constructor( } private fun onHardwareWalletClick() { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonHardwareUpdate) + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonHardwareUpdate()) router.push(AppRoute.WalletHardwareBackup(params.userWalletId)) } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt index f7e8fff31f..31d5ebdf25 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt @@ -4,6 +4,8 @@ import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic +import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -49,6 +51,7 @@ internal class WalletHardwareBackupModel @Inject constructor( private val urlOpener: UrlOpener, private val getUserWalletUseCase: GetUserWalletUseCase, private val messageSender: UiMessageSender, + private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { @@ -114,10 +117,16 @@ internal class WalletHardwareBackupModel @Inject constructor( ) init { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.HardwareBackupScreenOpened) + trackingContextProxy.addHotWalletContext() + analyticsEventHandler.send(WalletSettingsAnalyticEvents.HardwareBackupScreenOpened()) showPurchaseBlockWithDelay() } + override fun onDestroy() { + trackingContextProxy.removeContext() + super.onDestroy() + } + private fun showPurchaseBlockWithDelay() { modelScope.launch { delay(SHOW_PURCHASE_BLOCK_DELAY) @@ -126,7 +135,7 @@ internal class WalletHardwareBackupModel @Inject constructor( } private fun onCreateNewWalletClick() { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonCreateNewWallet) + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonCreateNewWallet()) router.push(AppRoute.CreateHardwareWallet) } @@ -134,7 +143,7 @@ internal class WalletHardwareBackupModel @Inject constructor( val userWallet = getUserWalletUseCase.invoke(params.userWalletId) .getOrElse { error("Cannot find user wallet with id: ${params.userWalletId.stringValue}") } if (userWallet is UserWallet.Hot) { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonUpgradeCurrent) + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonUpgradeCurrent()) if (!userWallet.backedUp) { messageSender.send(makeBackupAtFirstAlertBS) } else { @@ -160,6 +169,7 @@ internal class WalletHardwareBackupModel @Inject constructor( } private fun onBuyClick() { + analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.HardwareWallet)) modelScope.launch { generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } } diff --git a/features/manage-tokens/impl/detekt-baseline-debug.xml b/features/manage-tokens/impl/detekt-baseline-debug.xml index d580e5706e..6c3a7af5c8 100644 --- a/features/manage-tokens/impl/detekt-baseline-debug.xml +++ b/features/manage-tokens/impl/detekt-baseline-debug.xml @@ -15,7 +15,6 @@ MultilineLambdaItParameter:ChooseManagedTokenContent.kt${ add( CurrencyItemUM.Basic( id = ManagedCryptoCurrency.ID( value = "ID+$it", ), name = "Bitcoin", symbol = "BTC", icon = CurrencyIconState.Loading, networks = CurrencyItemUM.Basic.NetworksUM.Collapsed, onExpandClick = {}, ), ) } MultilineLambdaItParameter:ChooseManagedTokensModel.kt$ChooseManagedTokensModel${ it.copy( notificationUM = null, ) } MultilineLambdaItParameter:CurrencyItemMapper.kt${ it.toCurrencyNetworkModel( isSelected = it.network in addedIn, isEditable = false, onSelectedStateChange = { _, _ -> }, onLongTap = { _ -> }, ) } - MultilineLambdaItParameter:CurrencyNetworksMapper.kt${ it.toCurrencyNetworkModel( isSelected = it.network in addedIn, isEditable = isItemsEditable, onSelectedStateChange = onSelectedStateChange, onLongTap = onLongTap, ) } MultilineLambdaItParameter:CustomCurrencyFormOperations.kt${ it[Field.CONTRACT_ADDRESS] = it.getValue(Field.CONTRACT_ADDRESS).copy( error = when (exception) { CustomTokenFormValidationException.ContractAddress.Invalid -> { resourceReference(R.string.custom_token_creation_error_invalid_contract_address) } }, ) } MultilineLambdaItParameter:CustomCurrencyFormOperations.kt${ it[Field.DECIMALS] = it.getValue(Field.DECIMALS).copy( error = when (exception) { is CustomTokenFormValidationException.Decimals.Empty -> { null // Should not display this error } is CustomTokenFormValidationException.Decimals.Invalid -> { resourceReference( R.string.custom_token_creation_error_wrong_decimals, wrappedList(ValidateTokenFormUseCase.MAX_DECIMALS), ) } }, ) } MultilineLambdaItParameter:CustomTokenFormContent.kt$PreviewCustomTokenFormComponentProvider${ it[Field.CONTRACT_ADDRESS] = it[Field.CONTRACT_ADDRESS]!!.copy( label = stringReference("Contract address"), value = "0x1234567890", error = stringReference("Contract address is invalid"), placeholder = stringReference("0x1234567890"), ) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt index a2c559ac76..c3a41d047c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt @@ -31,6 +31,19 @@ internal sealed class CustomTokenAnalyticsEvent( ), ) + class AddTokenToAnotherAccount( + currencySymbol: String, + derivationPath: String, + source: ManageTokensSource, + ) : CustomTokenAnalyticsEvent( + event = "Button - Add Token To Another Account", + params = mapOf( + AnalyticsParam.Key.TOKEN_PARAM to currencySymbol, + AnalyticsParam.Key.DERIVATION to derivationPath, + AnalyticsParam.Key.SOURCE to source.analyticsName, + ), + ) + class NetworkSelected(networkName: String, source: ManageTokensSource) : CustomTokenAnalyticsEvent( event = "Custom Token Network Selected", params = mapOf( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/ManageTokensAnalyticEvent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/ManageTokensAnalyticEvent.kt index 4ef52f5c05..01f9d2a5d0 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/ManageTokensAnalyticEvent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/ManageTokensAnalyticEvent.kt @@ -49,7 +49,7 @@ internal sealed class ManageTokensAnalyticEvent( AnalyticsParam.Key.SOURCE to source.analyticsName, ), ) - data object ButtonLater : ManageTokensAnalyticEvent( + class ButtonLater : ManageTokensAnalyticEvent( event = "Button - Later", params = mapOf(AnalyticsParam.SOURCE to ManageTokensSource.ONBOARDING.analyticsName), ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt index 2baf3f982f..3fd1eb6250 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt @@ -2,6 +2,7 @@ package com.tangem.features.managetokens.component import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork @@ -16,7 +17,7 @@ internal interface CustomTokenFormComponent : ComposableContentComponent { val source: ManageTokensSource, val onSelectNetworkClick: (CustomTokenFormValues) -> Unit, val onSelectDerivationPathClick: (CustomTokenFormValues) -> Unit, - val onCurrencyAdded: () -> Unit, + val onCurrencyAdded: (currency: CryptoCurrency) -> Unit, ) interface Factory : ComponentFactory diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt index 8e270519a8..b49478560f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt @@ -15,6 +15,7 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent import com.tangem.features.managetokens.component.AddCustomTokenComponent @@ -232,7 +233,16 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( } } - private fun dismissAndNotify() { + private fun dismissAndNotify(currency: CryptoCurrency) { + val account = addedToAccount + if (account is Account.CryptoPortfolio && !account.isMainAccount) { + val event = CustomTokenAnalyticsEvent.AddTokenToAnotherAccount( + currencySymbol = currency.symbol, + derivationPath = currency.network.derivationPath.value.orEmpty(), + source = params.source, + ) + analyticsEventHandler.send(event) + } params.onCurrencyAdded(addedToAccount) dismiss() } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index 60b26282fb..ff7a6c3c36 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -364,7 +364,7 @@ internal class CustomTokenFormModel @Inject constructor( return@resource } - params.onCurrencyAdded() + params.onCurrencyAdded(currency) } private fun selectNetwork() { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt index ae218b4341..50267c156a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt @@ -287,7 +287,7 @@ internal class OnboardingManageTokensModel @Inject constructor( } }, ) { - analyticsEventHandler.send(ManageTokensAnalyticEvent.ButtonLater) + analyticsEventHandler.send(ManageTokensAnalyticEvent.ButtonLater()) useCasesFacade.saveManagedTokensUseCase( currenciesToAdd = manageTokensListManager.currenciesToAdd.value, diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt index 58e368cc07..0136e2532f 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt @@ -6,10 +6,10 @@ import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.markets.entry.BottomSheetState import kotlinx.serialization.Serializable @Stable diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/BottomSheetState.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/BottomSheetState.kt deleted file mode 100644 index 07aee6c5d9..0000000000 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/BottomSheetState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.features.markets.entry - -enum class BottomSheetState { - EXPANDED, - COLLAPSED, -} \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt index a30498b380..05a7e68670 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState @Stable interface MarketsEntryComponent { diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/tokenlist/MarketsTokenListComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/tokenlist/MarketsTokenListComponent.kt index 5a5179c1ef..8062a9a1f6 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/tokenlist/MarketsTokenListComponent.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/tokenlist/MarketsTokenListComponent.kt @@ -7,10 +7,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.markets.entry.BottomSheetState @Stable interface MarketsTokenListComponent : ComposableContentComponent { diff --git a/features/markets/impl/detekt-baseline-debug.xml b/features/markets/impl/detekt-baseline-debug.xml index 58ef1132a3..d9fe99460d 100644 --- a/features/markets/impl/detekt-baseline-debug.xml +++ b/features/markets/impl/detekt-baseline-debug.xml @@ -37,7 +37,6 @@ MultilineLambdaItParameter:AddToPortfolioBSContentUMFactory.kt$AddToPortfolioBSContentUMFactory${ if (it != selectedWalletId) { onAnotherWalletSelect(it) onWalletSelectorVisibilityChange(false) } } MultilineLambdaItParameter:AddToPortfolioBottomSheet.kt${ Content( modifier = Modifier.fillMaxWidth(), state = it, ) WalletSelectorBottomSheet(it.walletSelectorConfig) } MultilineLambdaItParameter:AddToPortfolioManager.kt$AddToPortfolioManager${ it.toMutableMap().apply { this[userWalletId] = if (isAddAction) { this[userWalletId].orEmpty() + network } else { this[userWalletId].orEmpty() - network } } } - MultilineLambdaItParameter:AddToPortfolioModel.kt$AddToPortfolioModel${ PortfolioData.CryptoCurrencyData( userWallet = selectedPortfolio.userWallet, status = addedToken, actions = it.states, ) } MultilineLambdaItParameter:AddToPortfolioModel.kt$AddToPortfolioModel${ Timber.e(it) params.callback.onDismiss() } MultilineLambdaItParameter:AddToPortfolioModel.kt$AddToPortfolioModel${ tokenActionsData.emit(it) navigation.replaceAll(AddToPortfolioRoutes.TokenActions) } MultilineLambdaItParameter:AddTokenModel.kt$AddTokenModel${ processError(error = it) uiState.value = um.toggleProgress(false) return@launch } @@ -58,7 +57,6 @@ MultilineLambdaItParameter:MarketsListBatchFlowManager.kt$MarketsListBatchFlowManager${ when (val status = it.status) { is PaginationStatus.Paginating -> { if (status.lastResult is BatchFetchResult.Success) { it.data.size == 1 } else { null } } is PaginationStatus.EndOfPagination -> { it.data.size == 1 } else -> null } } MultilineLambdaItParameter:MarketsListItem.kt${ if (Random.nextBoolean()) { it.first.inc() to PriceChangeType.UP } else { it.first.dec() to PriceChangeType.DOWN } } MultilineLambdaItParameter:MarketsListLazyColumn.kt${ (it.key as? String)?.split(TOKEN_LAZY_LIST_ID_SEPARATOR)?.first() ?.let { rawId -> CryptoCurrency.RawID(rawId) } } - MultilineLambdaItParameter:MarketsListModel.kt$MarketsListModel${ if (it == BottomSheetState.EXPANDED) { analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened) } } MultilineLambdaItParameter:MarketsListModel.kt$MarketsListModel${ if (it) { analyticsEventHandler.send(MarketsListAnalyticsEvent.TokenSearched(tokenFound = false)) } } MultilineLambdaItParameter:MarketsListModel.kt$MarketsListModel${ if (it.isNotEmpty()) { activeListManager.getBatchKeysByItemIds(visibleItemIds.value) } else { null } } MultilineLambdaItParameter:MarketsListModel.kt$MarketsListModel${ if (it.list !is ListUM.Content) { visibleItemIds.value = emptyList() } } @@ -118,12 +116,6 @@ NoNameShadowing:MarketsTokenDetailsModel.kt$MarketsTokenDetailsModel${ it.copy( chartState = it.chartState.copy( status = MarketsTokenDetailsUM.ChartState.Status.ERROR, ), body = if (it.body is MarketsTokenDetailsUM.Body.Error) { MarketsTokenDetailsUM.Body.Nothing } else { it.body }, ) } NoNameShadowing:MyPortfolioUMFactory.kt$MyPortfolioUMFactory${ networkIds.contains(it.status.currency.network.backendId) } NoNameShadowing:NewMarketsPortfolioDelegate.kt$NewMarketsPortfolioDelegate$portfolio - NonBooleanPropertyPrefixedWithIs:MarketsListBatchFlowManager.kt$MarketsListBatchFlowManager$val isInInitialLoadingErrorState = batchFlow.state .map { it.status is PaginationStatus.InitialLoadingError } .distinctUntilChanged() .stateIn( scope = modelScope, started = SharingStarted.Eagerly, initialValue = false, ) - NonBooleanPropertyPrefixedWithIs:MarketsListBatchFlowManager.kt$MarketsListBatchFlowManager$val isSearchNotFoundState = batchFlow.state .map { currentSearchText().isNullOrEmpty().not() && it.status is PaginationStatus.EndOfPagination && it.data.isEmpty() } .distinctUntilChanged() .stateIn( scope = modelScope, started = SharingStarted.Eagerly, initialValue = false, ) - NonBooleanPropertyPrefixedWithIs:MarketsListModel.kt$MarketsListModel$val isVisibleOnScreen = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:MarketsListUMStateManager.kt$MarketsListUMStateManager$val isInSearchStateFlow = state.map { it.searchBar.isActive }.distinctUntilChanged() - NonBooleanPropertyPrefixedWithIs:MarketsTokenDetailsModel.kt$MarketsTokenDetailsModel$val isVisibleOnScreen = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:TokenActionsHandler.kt$TokenActionsHandler$private val isDemoCardUseCase: IsDemoCardUseCase NullableToStringCall:MarketsListItemUM.kt$MarketsListItemUM$marketCap.toString() PropertyUsedBeforeDeclaration:MarketsListModel.kt$MarketsListModel$activeListManager PropertyUsedBeforeDeclaration:MarketsListUMStateManager.kt$MarketsListUMStateManager$state diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt index b1f66a3c20..54aa223690 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt @@ -11,6 +11,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles @@ -21,7 +22,6 @@ import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalytics import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel import com.tangem.features.markets.details.impl.model.state.TokenNetworksState import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent -import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt index 06f3b26d19..9f441298d5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt @@ -15,10 +15,10 @@ import com.arkivanov.decompose.value.Value import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.navigation.inner.InnerRouter +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child import com.tangem.features.markets.entry.impl.ui.EntryBottomSheetContent diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt index 9c72accb3a..a19e160096 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt @@ -15,10 +15,10 @@ import com.arkivanov.decompose.extensions.compose.stack.Children import com.arkivanov.decompose.extensions.compose.stack.animation.slide import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory import com.tangem.features.markets.tokenlist.MarketsTokenListComponent diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt index f165c1bb40..40c339c730 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt @@ -4,6 +4,7 @@ import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.popToFirst import com.arkivanov.decompose.router.stack.pushNew import com.arkivanov.decompose.router.stack.replaceAll +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -47,6 +48,7 @@ internal class AddToPortfolioModel @Inject constructor( private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCaseV2, private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency, private val messageSender: UiMessageSender, + private val analyticsEventHandler: AnalyticsEventHandler, val portfolioSelectorController: PortfolioSelectorController, ) : Model(), ChooseNetworkComponent.Callbacks by callbackDelegate, @@ -93,6 +95,7 @@ internal class AddToPortfolioModel @Inject constructor( .map { it.availableToAddData } .distinctUntilChanged() .stateIn(this) + val isAccountMode = portfolioSelectorController.isAccountModeSync() // use snapshot data, looks like we don’t need to remap at runtime val data = featureDataFlow.value @@ -117,6 +120,7 @@ internal class AddToPortfolioModel @Inject constructor( // force select a portfolio, triggers [selectedPortfolio] portfolioSelectorController.selectAccount(accountId) } else { + logAccountSelector(isAccountMode) navigation.replaceAll(AddToPortfolioRoutes.PortfolioSelector) } @@ -165,6 +169,7 @@ internal class AddToPortfolioModel @Inject constructor( .onEach { middleNavigationJob?.cancel() middleNavigationJob = changePortfolioNavigationFlow(data).launchIn(this) + logAccountSelector(isAccountMode) navigation.pushNew(AddToPortfolioRoutes.PortfolioSelector) } .launchIn(this) @@ -194,6 +199,12 @@ internal class AddToPortfolioModel @Inject constructor( .launchIn(modelScope) } + private fun logAccountSelector(isAccountMode: Boolean) { + if (isAccountMode) { + analyticsEventHandler.send(eventBuilder.popupToChooseAccount()) + } + } + private fun changeNetworkNavigationFlow(): Flow { return setupNetworkFlow(selectedPortfolio) .onEach { newNetwork -> @@ -260,6 +271,7 @@ internal class AddToPortfolioModel @Inject constructor( data.availableToAddWallets[selectedAccountId.userWalletId] ?: return@combine null val availableToAddAccount = availableToAddWallets.availableToAddAccounts[selectedAccountId] ?: return@combine null + if (!isAccountMode) analyticsEventHandler.send(eventBuilder.addToPortfolioWalletChanged()) SelectedPortfolio( isAccountMode = isAccountMode, userWallet = availableToAddWallets.userWallet, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt index 8a4ec65436..b0f322b6f5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.markets.portfolio.add.impl.model +import com.tangem.common.ui.addtoken.AddTokenUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -10,13 +11,13 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.ToastMessage import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase +import com.tangem.domain.models.account.Account import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.add.api.SelectedNetwork import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio import com.tangem.features.markets.portfolio.add.impl.AddTokenComponent import com.tangem.features.markets.portfolio.add.impl.model.AddTokenUiBuilder.Companion.toggleProgress -import com.tangem.common.ui.addtoken.AddTokenUM import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder @@ -74,7 +75,8 @@ internal class AddTokenModel @Inject constructor( analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames)) val cryptoCurrency = selectedNetwork.cryptoCurrency - val accountId = selectedPortfolio.account.account.account.accountId + val account = selectedPortfolio.account.account.account + val accountId = account.accountId manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency) .onLeft { processError(error = it) @@ -90,6 +92,11 @@ internal class AddTokenModel @Inject constructor( if (status == null) { processError(error = null) } else { + when (account) { + is Account.CryptoPortfolio -> if (!account.isMainAccount) { + analyticsEventHandler.send(analyticsEventBuilder.addToNotMainAccount()) + } + } params.callbacks.onTokenAdded(status.status) } uiState.value = um.toggleProgress(false) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsModel.kt index a2d0ceeba2..d3dd8d929b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsModel.kt @@ -62,10 +62,7 @@ internal class TokenActionsModel @Inject constructor( ) private fun handledQuickAction(handledAction: HandledQuickAction) { - val event = analyticsEventBuilder.quickActionClick( - actionUM = handledAction.action, - blockchainName = handledAction.cryptoCurrencyData.status.currency.network.name, - ) + val event = analyticsEventBuilder.getTokenActionClick(actionUM = handledAction.action) analyticsEventHandler.send(event) val isReceive = handledAction.action == TokenActionsBSContentUM.Action.Receive if (!isReceive) return diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsUiBuilder.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsUiBuilder.kt index be2f06ccdf..6a1dcb993e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsUiBuilder.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsUiBuilder.kt @@ -1,5 +1,6 @@ package com.tangem.features.markets.portfolio.add.impl.model +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter @@ -15,6 +16,7 @@ import javax.inject.Inject @ModelScoped internal class TokenActionsUiBuilder @Inject constructor( paramsContainer: ParamsContainer, + private val analyticsEventHandler: AnalyticsEventHandler, ) { private val params = paramsContainer.require() @@ -32,7 +34,10 @@ internal class TokenActionsUiBuilder @Inject constructor( ) return TokenActionsUM( token = tokenUM, - onLaterClick = { params.callbacks.onLaterClick() }, + onLaterClick = { + analyticsEventHandler.send(params.eventBuilder.getTokenLater()) + params.callbacks.onLaterClick() + }, quickActions = PortfolioTokenUMConverter.quickActions(data, tokenActionsHandler), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt index c68c7b7ad5..9c82131fe6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt @@ -31,7 +31,7 @@ internal class DefaultAddToPortfolioManager @AssistedInject constructor( override val allAvailableNetworks: Flow> = _allAvailableNetworks.asSharedFlow() override val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create( - mode = PortfolioFetcher.Mode.All(onlyMultiCurrency = true), + mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true), scope = scope, ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt index 9e02ec98a2..772d83f651 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt @@ -21,6 +21,14 @@ internal class PortfolioAnalyticsEvent( ), ) + fun popupToChooseAccount() = PortfolioAnalyticsEvent( + event = "Popup to choose account", + ) + + fun addToNotMainAccount() = PortfolioAnalyticsEvent( + event = "Button - Add (token not to main Account)", + ) + fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent(event = "Wallet Selected") fun addToPortfolioContinue(blockchainNames: List) = PortfolioAnalyticsEvent( @@ -47,5 +55,19 @@ internal class PortfolioAnalyticsEvent( put("blockchain", blockchainName) }, ) + + fun getTokenActionClick(actionUM: TokenActionsBSContentUM.Action) = PortfolioAnalyticsEvent( + event = when (actionUM) { + TokenActionsBSContentUM.Action.Buy -> "Popup Get token - Button Buy" + TokenActionsBSContentUM.Action.Receive -> "Popup Get token - Button Receive" + TokenActionsBSContentUM.Action.Exchange -> "Popup Get token - Button Exchange" + TokenActionsBSContentUM.Action.Stake -> "Popup Get token - Button Stake" + else -> "error" + }, + ) + + fun getTokenLater() = PortfolioAnalyticsEvent( + event = "Popup Get token - Button Later", + ) } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index d5e434ad2a..a314e9a0e1 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -193,7 +193,10 @@ internal class MarketsPortfolioModel @Inject constructor( NewAddToPortfolioManager.State.NothingToAdd -> AddButtonState.Unavailable } }, - onAddClick = { bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio) }, + onAddClick = { + analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioClicked()) + bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio) + }, ) newMarketsPortfolioDelegate.combineData() .onEach { _state.value = it } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt index 8e6955c9b0..14e1c4dc72 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt @@ -16,12 +16,12 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute.MarketsTokenDetails.AnalyticsParams import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.markets.toSerializableParam -import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.tokenlist.MarketsTokenListComponent import com.tangem.features.markets.tokenlist.impl.model.MarketsListModel import com.tangem.features.markets.tokenlist.impl.ui.MarketsList diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt index f1916f48bb..c5f92d0700 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/analytics/MarketsListAnalyticsEvent.kt @@ -9,7 +9,7 @@ internal sealed class MarketsListAnalyticsEvent( params: Map = mapOf(), ) : AnalyticsEvent(category = "Markets", event = event, params = params) { - data object BottomSheetOpened : MarketsListAnalyticsEvent(event = "Markets Screen Opened") + class BottomSheetOpened : MarketsListAnalyticsEvent(event = "Markets Screen Opened") data class SortBy( val sortByTypeUM: SortByTypeUM, @@ -33,11 +33,11 @@ internal sealed class MarketsListAnalyticsEvent( ), ) - data object StakingPromoShown : MarketsListAnalyticsEvent(event = "Notice - Staking Promo") + class StakingPromoShown : MarketsListAnalyticsEvent(event = "Notice - Staking Promo") - data object StakingPromoClosed : MarketsListAnalyticsEvent(event = "Staking Promo Closed") + class StakingPromoClosed : MarketsListAnalyticsEvent(event = "Staking Promo Closed") - data object StakingMoreInfoClicked : MarketsListAnalyticsEvent(event = "Staking More Info") + class StakingMoreInfoClicked : MarketsListAnalyticsEvent(event = "Staking More Info") data class TokenSearched(val tokenFound: Boolean) : MarketsListAnalyticsEvent( event = "Token Searched", @@ -46,5 +46,5 @@ internal sealed class MarketsListAnalyticsEvent( ), ) - data object ShowTokens : MarketsListAnalyticsEvent(event = "Button - Show Tokens") + class ShowTokens : MarketsListAnalyticsEvent(event = "Button - Show Tokens") } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt index 2018d1eb53..600133bc7c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt @@ -6,6 +6,7 @@ import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase @@ -17,7 +18,6 @@ import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.UserCountryError import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions -import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager @@ -69,9 +69,9 @@ internal class MarketsListModel @Inject constructor( visibleItemsChanged = { visibleItemIds.value = it }, onRetryButtonClicked = { activeListManager.reload() }, onTokenClick = { onTokenUIClicked(it) }, - onStakingNotificationClick = { analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingMoreInfoClicked) }, + onStakingNotificationClick = { analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingMoreInfoClicked()) }, onStakingNotificationCloseClick = { onStakingNotificationCloseClick() }, - onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens) }, + onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens()) }, ) private val mainMarketsListManager = MarketsListBatchFlowManager( @@ -150,7 +150,7 @@ internal class MarketsListModel @Inject constructor( if (marketsListUMStateManager.state.value.stakingNotificationMaxApy == null && stakingNotificationMaxApy != null ) { - analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoShown) + analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoShown()) } marketsListUMStateManager.onUiItemsChanged( @@ -267,9 +267,9 @@ internal class MarketsListModel @Inject constructor( } private fun initAnalytics() { - containerBottomSheetState.onEach { - if (it == BottomSheetState.EXPANDED) { - analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened) + containerBottomSheetState.onEach { bottomSheetState -> + if (bottomSheetState == BottomSheetState.EXPANDED) { + analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened()) } }.launchIn(modelScope) @@ -303,7 +303,7 @@ internal class MarketsListModel @Inject constructor( } private fun onStakingNotificationCloseClick() { - analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoClosed) + analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoClosed()) modelScope.launch { promoRepository.setMarketsStakingNotificationHideClicked() } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index 041df48f96..5b80958c46 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -30,6 +30,7 @@ import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition @@ -46,7 +47,6 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.impl.R import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListSortByBottomSheet diff --git a/features/news/news-details/api/build.gradle.kts b/features/news/news-details/api/build.gradle.kts new file mode 100644 index 0000000000..b68f6815c6 --- /dev/null +++ b/features/news/news-details/api/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.news.details.api" +} + +dependencies { + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) +} diff --git a/features/news/news-details/api/src/main/kotlin/com/tangem/features/news/details/api/NewsDetailsComponent.kt b/features/news/news-details/api/src/main/kotlin/com/tangem/features/news/details/api/NewsDetailsComponent.kt new file mode 100644 index 0000000000..167f65b88f --- /dev/null +++ b/features/news/news-details/api/src/main/kotlin/com/tangem/features/news/details/api/NewsDetailsComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.news.details.api + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface NewsDetailsComponent : ComposableContentComponent { + + data class Params(val selectedArticleId: Int = 0) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/news/news-details/impl/build.gradle.kts b/features/news/news-details/impl/build.gradle.kts new file mode 100644 index 0000000000..a9953868af --- /dev/null +++ b/features/news/news-details/impl/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.news.details.impl" +} + +dependencies { + /* AndroidX */ + implementation(deps.lifecycle.compose) + implementation(deps.androidx.activity.compose) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.material3) + + /** Core modules */ + implementation(projects.core.ui) + implementation(projects.core.utils) + implementation(projects.core.decompose) + implementation(projects.common.ui) + implementation(projects.common.routing) + + /** Feature modules */ + implementation(projects.features.news.newsDetails.api) + implementation(projects.domain.models) + + /** Other dependencies */ + implementation(deps.kotlin.immutable.collections) + implementation(deps.arrow.core) + implementation(deps.timber) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/DefaultNewsDetailsComponent.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/DefaultNewsDetailsComponent.kt new file mode 100644 index 0000000000..d68562beb6 --- /dev/null +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/DefaultNewsDetailsComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.news.details.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.news.details.api.NewsDetailsComponent +import com.tangem.features.news.details.impl.ui.NewsDetailsContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultNewsDetailsComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: NewsDetailsComponent.Params, +) : NewsDetailsComponent, AppComponentContext by context { + + private val model: NewsDetailsModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val uiState by model.uiState.collectAsState() + NewsDetailsContent( + state = uiState, + onBackClick = model::onBackClick, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : NewsDetailsComponent.Factory { + override fun create( + context: AppComponentContext, + params: NewsDetailsComponent.Params, + ): DefaultNewsDetailsComponent + } +} \ No newline at end of file diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/MockArticlesFactory.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/MockArticlesFactory.kt new file mode 100644 index 0000000000..ac35bb6ee4 --- /dev/null +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/MockArticlesFactory.kt @@ -0,0 +1,182 @@ +package com.tangem.features.news.details.impl + +import com.tangem.core.ui.components.label.entity.LabelSize +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.news.details.impl.ui.ArticleUM +import com.tangem.features.news.details.impl.ui.SourceUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +// TODO [REDACTED_TASK_KEY] remove mock data +@Suppress("MaximumLineLength", "LongMethod") +object MockArticlesFactory { + fun createMockArticles(): ImmutableList = listOf( + ArticleUM( + id = 1, + title = "SEC delays decisions on ETH-staking ETFs and spot XRP/SOL funds", + createdAt = "20 Jun, 21:45", + score = 6.5f, + tags = listOf( + LabelUM(text = TextReference.Str("Regulation"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Bitwise has updated its Solana ETF, adding staking and setting a low management fee of 0.20%.", + content = "Bitwise Asset Management has revised its filing to launch a Solana ETF, renaming it \"Bitwise Solana Staking ETF\" and setting an exceptionally low management fee of just 0.20%.\n\nThe update comes as the SEC prepares to review several Solana ETF applications.", + sources = listOf( + SourceUM( + id = 1, + title = "Deeper liquidity could drive crypto market beyond \$6T", + sourceName = "cointelegraph", + publishedAt = "1h ago", + url = "https://cointelegraph.com", + ), + SourceUM( + id = 2, + title = "Top gainers and losers in crypto this week", + sourceName = "Investing", + publishedAt = "2h ago", + url = "https://investing.com", + ), + ).toPersistentList(), + ), + ArticleUM( + id = 2, + title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)", + createdAt = "20 Jun, 20:15", + score = 8.2f, + tags = listOf( + LabelUM(text = TextReference.Str("BTC"), size = LabelSize.BIG), + LabelUM(text = TextReference.Str("ETF"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Bitcoin spot ETFs recorded fourth consecutive day of positive inflows.", + content = "Bitcoin spot ETFs continue their impressive streak with \$550 million in net positive inflows.\n\nBlackRock's IBIT led with \$250M.", + sources = listOf( + SourceUM( + id = 3, + title = "Bitcoin ETFs see massive inflows", + sourceName = "Bloomberg", + publishedAt = "3h ago", + url = "https://bloomberg.com", + ), + ).toPersistentList(), + ), + ArticleUM( + id = 3, + title = "Ethereum network upgrade scheduled for Q2 2025", + createdAt = "20 Jun, 18:30", + score = 7.8f, + tags = listOf( + LabelUM(text = TextReference.Str("ETH"), size = LabelSize.BIG), + LabelUM(text = TextReference.Str("Technology"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Ethereum developers announced major network upgrade.", + content = "The Ethereum Foundation announced a significant upgrade for Q2 2025.\n\nKey improvements include EVM enhancements.", + sources = listOf( + SourceUM( + id = 4, + title = "Ethereum core devs announce upgrade", + sourceName = "CoinDesk", + publishedAt = "5h ago", + url = "https://coindesk.com", + ), + ).toPersistentList(), + ), + ArticleUM( + id = 4, + title = "Solana surpasses Ethereum in daily transaction volume", + createdAt = "20 Jun, 16:00", + score = 9.1f, + tags = listOf( + LabelUM(text = TextReference.Str("SOL"), size = LabelSize.BIG), + LabelUM(text = TextReference.Str("Market"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Solana achieved new milestone processing more daily transactions than Ethereum.", + content = "Solana processed over 50 million transactions in a single day.\n\nDriven by DeFi and NFT activity.", + sources = listOf( + SourceUM( + id = 5, + title = "Solana transactions hit record", + sourceName = "The Block", + publishedAt = "7h ago", + url = "https://theblock.co", + ), + ).toPersistentList(), + ), + ArticleUM( + id = 5, + title = "DeFi protocol launches innovative yield farming strategy", + createdAt = "20 Jun, 14:20", + score = 6.9f, + tags = listOf( + LabelUM(text = TextReference.Str("DeFi"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "New DeFi protocol introduced innovative yield farming approach.", + content = "A newly launched protocol unveiled innovative yield farming mechanism.\n\nAPY rates range from 15% to 30%.", + sources = persistentListOf(), + ), + ArticleUM( + id = 6, + title = "Crypto regulation bill advances in US Senate", + createdAt = "20 Jun, 12:45", + score = 8.7f, + tags = listOf( + LabelUM(text = TextReference.Str("Regulation"), size = LabelSize.BIG), + LabelUM(text = TextReference.Str("USA"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Comprehensive cryptocurrency regulation bill passed Senate Banking Committee.", + content = "The US Senate Banking Committee advanced landmark crypto regulation bill.\n\nKey provisions include asset definitions.", + sources = persistentListOf(), + ), + ArticleUM( + id = 7, + title = "Major bank announces crypto custody services", + createdAt = "20 Jun, 10:30", + score = 7.3f, + tags = listOf( + LabelUM(text = TextReference.Str("Adoption"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "World's largest bank announced cryptocurrency custody services.", + content = "Major financial institution announced comprehensive crypto custody services.\n\nSupporting Bitcoin and Ethereum initially.", + sources = persistentListOf(), + ), + ArticleUM( + id = 8, + title = "NFT marketplace reports 300% increase in trading volume", + createdAt = "20 Jun, 08:15", + score = 5.8f, + tags = listOf( + LabelUM(text = TextReference.Str("NFT"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Leading NFT marketplace experienced dramatic surge in trading activity.", + content = "Prominent NFT marketplace reported 300% increase in trading volume.\n\nNew features include lower fees.", + sources = persistentListOf(), + ), + ArticleUM( + id = 9, + title = "Layer 2 solution achieves 100,000 TPS milestone", + createdAt = "19 Jun, 22:00", + score = 8.5f, + tags = listOf( + LabelUM(text = TextReference.Str("Technology"), size = LabelSize.BIG), + LabelUM(text = TextReference.Str("L2"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "New Layer 2 scaling solution achieved 100,000 TPS in testing.", + content = "Layer 2 solution processed 100,000 transactions per second.\n\nUsing zero-knowledge proof technology.", + sources = persistentListOf(), + ), + ArticleUM( + id = 10, + title = "Stablecoin market cap reaches new all-time high", + createdAt = "19 Jun, 19:30", + score = 7.6f, + tags = listOf( + LabelUM(text = TextReference.Str("Stablecoins"), size = LabelSize.BIG), + LabelUM(text = TextReference.Str("Market"), size = LabelSize.BIG), + ).toPersistentList(), + shortContent = "Total stablecoin market capitalization surpassed \$180 billion.", + content = "Stablecoin market cap reached \$180 billion all-time high.\n\nDriven by DeFi activity and institutional adoption.", + sources = persistentListOf(), + ), + ).toPersistentList() +} \ No newline at end of file diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/NewsDetailsModel.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/NewsDetailsModel.kt new file mode 100644 index 0000000000..3ff89dde9d --- /dev/null +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/NewsDetailsModel.kt @@ -0,0 +1,41 @@ +package com.tangem.features.news.details.impl + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.features.news.details.api.NewsDetailsComponent +import com.tangem.features.news.details.impl.ui.NewsDetailsUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.indexOfFirstOrNull +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@ModelScoped +internal class NewsDetailsModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, +) : Model() { + + private val mockedArticles = MockArticlesFactory.createMockArticles() + + private val params = paramsContainer.require() + + private val _uiState = MutableStateFlow( + NewsDetailsUM( + /* [REDACTED_TODO_COMMENT] */ + articles = mockedArticles, + selectedArticleIndex = mockedArticles.indexOfFirstOrNull { it.id == params.selectedArticleId } ?: 0, + onShareClick = { /* [REDACTED_TODO_COMMENT] */ }, + onLikeClick = { /* [REDACTED_TODO_COMMENT] */ }, + ), + ) + val uiState: StateFlow = _uiState.asStateFlow() + + fun onBackClick() { + router.pop() + } +} \ No newline at end of file diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/di/NewsDetailsModule.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/di/NewsDetailsModule.kt new file mode 100644 index 0000000000..f0fcac783e --- /dev/null +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/di/NewsDetailsModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.news.details.impl.di + +import com.tangem.features.news.details.api.NewsDetailsComponent +import com.tangem.features.news.details.impl.DefaultNewsDetailsComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface NewsDetailsModule { + + @Binds + @Singleton + fun bindNewsDetailsComponentFactory(factory: DefaultNewsDetailsComponent.Factory): NewsDetailsComponent.Factory +} \ No newline at end of file diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsContent.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsContent.kt new file mode 100644 index 0000000000..a149f57bf4 --- /dev/null +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsContent.kt @@ -0,0 +1,278 @@ +package com.tangem.features.news.details.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.news.ArticleHeader +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SecondaryButtonIconStart +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.pager.PagerIndicator +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.news.details.impl.MockArticlesFactory + +// TODO [REDACTED_TASK_KEY] make internal +@Composable +fun NewsDetailsContent(state: NewsDetailsUM, onBackClick: () -> Unit, modifier: Modifier = Modifier) { + val pagerState = rememberPagerState( + initialPage = state.selectedArticleIndex, + pageCount = { state.articles.size }, + ) + + Column( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.secondary) + .systemBarsPadding(), + ) { + Column { + TangemTopAppBar( + title = null, + startButton = TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_back_24, + onClicked = onBackClick, + ), + endButton = TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_share_24, + onClicked = state.onShareClick, + ), + ) + Box( + modifier = Modifier.fillMaxSize(), + ) { + if (state.articles.isNotEmpty()) { + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize(), + ) { page -> + ArticleDetail( + article = state.articles[page], + modifier = Modifier.fillMaxSize(), + onLikeClick = state.onLikeClick, + ) + } + + if (state.articles.size > 1) { + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth(), + ) { + PagerIndicator( + pagerState = pagerState, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(bottom = 16.dp), + ) + } + } + } + } + } + } +} + +@Suppress("LongMethod") +@Composable +private fun ArticleDetail(article: ArticleUM, modifier: Modifier = Modifier, onLikeClick: () -> Unit) { + val density = LocalDensity.current + val pagerHeight = 48.dp + val contentPadding = 56.dp + LazyColumn( + modifier = modifier.padding(horizontal = 16.dp), + contentPadding = PaddingValues( + bottom = contentPadding + pagerHeight + WindowInsets.navigationBars.getBottom(density).dp, + ), + ) { + item { + ArticleHeader( + title = article.title, + createdAt = article.createdAt, + score = article.score, + tags = article.tags, + modifier = Modifier.padding(top = 16.dp), + ) + + if (article.shortContent.isNotEmpty()) { + QuickRecap( + content = article.shortContent, + modifier = Modifier.padding(top = 32.dp), + ) + } + + Text( + text = article.content, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(top = 16.dp), + ) + + Spacer(modifier = Modifier.height(24.dp)) + + SecondaryButtonIconStart( + iconResId = R.drawable.ic_heart_20, + text = "Like", // TODO [REDACTED_TASK_KEY] export to strings + size = TangemButtonSize.RoundedAction, + onClick = onLikeClick, + ) + + // TODO [REDACTED_TASK_KEY] add related tokens block + + if (article.sources.isNotEmpty()) { + Spacer(modifier = Modifier.height(24.dp)) + Row { + Text( + text = "Sources", // TODO [REDACTED_TASK_KEY] export to strings + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "${article.sources.size}", + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.tertiary, + ) + } + } + } + + if (article.sources.isNotEmpty()) { + item { + val sourcesPagerState = rememberPagerState( + pageCount = { article.sources.size }, + ) + Spacer(modifier = Modifier.height(12.dp)) + HorizontalPager( + state = sourcesPagerState, + modifier = Modifier + .fillMaxWidth(), + pageSpacing = 12.dp, + contentPadding = PaddingValues(horizontal = 0.dp), + ) { page -> + SourceItem( + source = article.sources[page], + modifier = Modifier.fillMaxWidth(), + ) + } + Spacer(modifier = Modifier.height(12.dp)) + } + } + } +} + +@Composable +private fun QuickRecap(content: String, modifier: Modifier = Modifier) { + Box( + modifier = modifier.height(IntrinsicSize.Min), + ) { + VerticalDivider( + modifier = Modifier.fillMaxHeight(), + thickness = 2.dp, + color = TangemTheme.colors.stroke.primary, + ) + Column( + modifier = Modifier + .padding(start = 16.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_quick_recap_16), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Quick recap", // TODO [REDACTED_TASK_KEY] export to strings + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.accent, + ) + } + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = content, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } + } +} + +@Composable +private fun SourceItem(source: SourceUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background( + color = TangemTheme.colors.background.primary, + shape = RoundedCornerShape(12.dp), + ) + .padding(12.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 4.dp), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_explore_16), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = source.sourceName, + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + } + if (source.title.isNotEmpty()) { + Text( + text = source.title, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(bottom = 12.dp), + ) + } + Text( + text = source.publishedAt, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewNewsDetailsContent() { + TangemThemePreview { + NewsDetailsContent( + state = NewsDetailsUM( + articles = MockArticlesFactory.createMockArticles(), + selectedArticleIndex = 0, + onShareClick = { }, + onLikeClick = { }, + ), + onBackClick = { }, + ) + } +} \ No newline at end of file diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsUM.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsUM.kt new file mode 100644 index 0000000000..889fcc202a --- /dev/null +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsUM.kt @@ -0,0 +1,33 @@ +package com.tangem.features.news.details.impl.ui + +import com.tangem.core.ui.components.label.entity.LabelUM +import kotlinx.collections.immutable.ImmutableList + +// TODO [REDACTED_TASK_KEY] make internal +data class NewsDetailsUM( + val articles: ImmutableList, + val selectedArticleIndex: Int, + val onShareClick: () -> Unit, + val onLikeClick: () -> Unit, +) + +// TODO [REDACTED_TASK_KEY] make internal +data class ArticleUM( + val id: Int, + val title: String, + val createdAt: String, + val score: Float, + val tags: ImmutableList, + val shortContent: String, + val content: String, + val sources: ImmutableList, +) + +// TODO [REDACTED_TASK_KEY] make internal +data class SourceUM( + val id: Int, + val title: String, + val sourceName: String, + val publishedAt: String, + val url: String, +) \ No newline at end of file diff --git a/features/nft/impl/detekt-baseline-debug.xml b/features/nft/impl/detekt-baseline-debug.xml index 27148479aa..e079f03d2a 100644 --- a/features/nft/impl/detekt-baseline-debug.xml +++ b/features/nft/impl/detekt-baseline-debug.xml @@ -23,8 +23,6 @@ MultilineLambdaItParameter:NFTReceiveModel.kt$NFTReceiveModel${ it.copy( bottomSheetConfig = it.bottomSheetConfig?.copy(isShown = false), ) } MultilineLambdaItParameter:UpdateDataStateTransformer.kt$UpdateDataStateTransformer${ NFTCollectionUM( id = it.collectionIdProvider(), networkIconId = getActiveIconRes(it.network.rawId), name = it.name.orEmpty(), description = TextReference.PluralRes( R.plurals.nft_collections_count, it.count, wrappedList(it.count), ), logoUrl = it.logoUrl, assets = it.transformAssets(), onExpandClick = { onExpandCollectionClick(it) }, isExpanded = it.isExpanded(state), ) } NoNameShadowing:NFTCollectionsModel.kt$NFTCollectionsModel${ val assetsFulfillQuery = if (query.isEmpty()) { true } else { when (val assets = it.assets) { is NFTCollection.Assets.Empty, is NFTCollection.Assets.Failed, is NFTCollection.Assets.Loading, -> false is NFTCollection.Assets.Value -> { assets.items.any { asset -> asset.name?.lowercase()?.contains(query.lowercase()) == true } } } } val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true collectionFulfillQuery || assetsFulfillQuery } - NonBooleanPropertyPrefixedWithIs:NFTCollectionsModel.kt$NFTCollectionsModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:NFTReceiveModel.kt$NFTReceiveModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase NullableBooleanCheck:UpdateDataStateTransformer.kt$UpdateDataStateTransformer$(state.content as? NFTCollectionsUM.Content) ?.collections ?.filterIsInstance<NFTCollectionUM>() ?.firstOrNull { it.id == this.collectionIdProvider() } ?.isExpanded ?: false NullableToStringCall:NFTCollectionsContent.kt$${item2?.id} NullableToStringCall:NFTCollectionsModel.kt$NFTCollectionsModel$${network.derivationPath.value} diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt index 966c793b66..e47a5fca31 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt @@ -69,7 +69,7 @@ internal class NFTDetailsModel @Inject constructor( onBackClick = { params.onBackClick() }, onReadMoreClick = ::onReadMoreClick, onSeeAllTraitsClick = { - analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonSeeAll) + analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonSeeAll()) params.onAllTraitsClick() }, onExploreClick = ::onExploreClick, @@ -161,7 +161,7 @@ internal class NFTDetailsModel @Inject constructor( } private fun onInfoBlockClick(title: TextReference, text: TextReference) { - analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonReadMore) + analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonReadMore()) bottomSheetNavigation.activate( NFTDetailsBottomSheetConfig.Info( title = title, @@ -171,7 +171,7 @@ internal class NFTDetailsModel @Inject constructor( } private fun onReadMoreClick() { - analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonReadMore) + analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonReadMore()) when (val topInfo = _state.value.nftAsset.topInfo) { is NFTAssetUM.TopInfo.Empty -> Unit is NFTAssetUM.TopInfo.Content -> { @@ -186,7 +186,7 @@ internal class NFTDetailsModel @Inject constructor( } private fun onExploreClick() { - analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonExplore) + analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonExplore()) modelScope.launch { val url = getNFTExploreUrlUseCase.invoke( network = params.nftAsset.network, @@ -199,7 +199,7 @@ internal class NFTDetailsModel @Inject constructor( } private fun onSendClick() { - analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonSend) + analyticsEventHandler.send(NFTAnalyticsEvent.Details.ButtonSend()) router.push( AppRoute.NFTSend( userWalletId = params.userWalletId, diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt index e9ebb194f0..889afd28d6 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt @@ -89,7 +89,7 @@ internal class NFTReceiveModel @Inject constructor( val state: StateFlow get() = _state init { - analyticsEventHandler.send(NFTAnalyticsEvent.Receive.ScreenOpened) + analyticsEventHandler.send(NFTAnalyticsEvent.Receive.ScreenOpened()) subscribeToNFTAvailableNetworks() loadPortfolioName() } diff --git a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/util/ResetCardsComponent.kt b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/util/ResetCardsComponent.kt index bd3a3203e2..6524a947c0 100644 --- a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/util/ResetCardsComponent.kt +++ b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/util/ResetCardsComponent.kt @@ -8,8 +8,13 @@ interface ResetCardsComponent { fun startResetCardsFlow(createdUserWallet: UserWallet.Cold) data class Params( + val source: Source, val callbacks: ModelCallbacks, - ) + ) { + enum class Source { + Upgrade, Onboarding, + } + } interface ModelCallbacks { fun onCancel() diff --git a/features/onboarding-v2/impl/detekt-baseline-debug.xml b/features/onboarding-v2/impl/detekt-baseline-debug.xml index 3eced7c5d9..1cc4bb2e21 100644 --- a/features/onboarding-v2/impl/detekt-baseline-debug.xml +++ b/features/onboarding-v2/impl/detekt-baseline-debug.xml @@ -98,7 +98,6 @@ MultilineLambdaItParameter:OnboardingVisaModel.kt$OnboardingVisaModel${ val derivedKey = it.derivedKeys[VisaUtilities.visaDefaultDerivationPath] ?: return@any false VisaWalletPublicKeyUtility.validateExtendedPublicKey( targetAddress = targetAddress, extendedPublicKey = derivedKey, ).onLeft { return@any VisaWalletPublicKeyUtility.findKeyWithoutDerivation( targetAddress = targetAddress, card = wallet.scanResponse.card, ).isRight() }.isRight() } MultilineLambdaItParameter:OnboardingVisaOtherWalletModel.kt$OnboardingVisaOtherWalletModel${ if (it is VisaActivationRemoteState.AwaitingPinCode) { onDone.emit(it.activationOrderInfo) return@launch } } MultilineLambdaItParameter:OnboardingVisaOtherWalletModel.kt$OnboardingVisaOtherWalletModel${ uiMessageSender.showErrorDialog(it) analyticsEventHandler.send(VisaAnalyticsEvent.ErrorOnboarding(it)) delay(timeMillis = 60_000) } - MultilineLambdaItParameter:OnboardingVisaPinCodeModel.kt$OnboardingVisaPinCodeModel${ it.copy( pinCode = pin, submitButtonEnabled = PinCodeValidation.validate(pin), error = if (isError) { analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ErrorPinValidation) resourceReference(R.string.visa_onboarding_pin_validation_error_message) } else { null }, ) } MultilineLambdaItParameter:OnboardingVisaWelcomeModel.kt$OnboardingVisaWelcomeModel${ onError(it) return@launch } MultilineLambdaItParameter:SeedPhraseCheckUiStateBuilder.kt$SeedPhraseCheckUiStateBuilder${ checkWordField( word = it.word.text, shownIndex = it.index, ) } MultilineLambdaItParameter:TwinWalletArtwork.kt${ animationState = it val maxTime = maxOf( transition1.totalDurationNanos, transition2.totalDurationNanos, ) delay(TimeUnit.NANOSECONDS.toMillis(maxTime)) } @@ -107,7 +106,6 @@ NoNameShadowing:MultiWalletCreateWalletModel.kt$MultiWalletCreateWalletModel${ it.copy(resultUserWallet = userWallet) } NoNameShadowing:MultiWalletUpgradeWalletModel.kt$MultiWalletUpgradeWalletModel${ it.copy(resultUserWallet = userWallet) } NoNameShadowing:Wallet1ChooseOptionModel.kt$Wallet1ChooseOptionModel${ it.copy(resultUserWallet = userWallet) } - NonBooleanPropertyPrefixedWithIs:MultiWalletSeedPhraseModel.kt$MultiWalletSeedPhraseModel$private val isWalletAlreadySavedUseCase: IsWalletAlreadySavedUseCase PropertyUsedBeforeDeclaration:MultiWalletCreateWalletModel.kt$MultiWalletCreateWalletModel$onDone RedundantSuspendModifier:MultiWalletCreateWalletModel.kt$MultiWalletCreateWalletModel$suspend RedundantSuspendModifier:OnboardingNoteCreateWalletModel.kt$OnboardingNoteCreateWalletModel$suspend @@ -127,7 +125,6 @@ UseEmptyCounterpart:OnboardingEvent.kt$OnboardingEvent$mapOf() UseEmptyCounterpart:OnboardingEvent.kt$OnboardingEvent.Backup$mapOf() UseEmptyCounterpart:OnboardingEvent.kt$OnboardingEvent.CreateWallet$mapOf() - UseEmptyCounterpart:OnboardingEvent.kt$OnboardingEvent.Topup$mapOf() UseEmptyCounterpart:OnboardingEvent.kt$OnboardingEvent.Twins$mapOf() UseEmptyCounterpart:OnboardingVisaAnalyticsEvent.kt$OnboardingVisaAnalyticsEvent$mapOf() UseEmptyCounterpart:VisaAnalyticsEvent.kt$VisaAnalyticsEvent$mapOf() diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt index 33016dc2eb..1aa8695bab 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt @@ -2,7 +2,6 @@ package com.tangem.features.onboarding.v2.common.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.domain.models.currency.CryptoCurrency sealed class OnboardingEvent( category: String, @@ -10,23 +9,26 @@ sealed class OnboardingEvent( params: Map = mapOf(), ) : AnalyticsEvent(category, event, params) { - data object Started : OnboardingEvent("Onboarding", "Onboarding Started") - data object Finished : OnboardingEvent("Onboarding", "Onboarding Finished") + class Started : OnboardingEvent("Onboarding", "Onboarding Started") + class Finished : OnboardingEvent("Onboarding", "Onboarding Finished") sealed class CreateWallet( event: String, params: Map = mapOf(), ) : OnboardingEvent("Onboarding / Create Wallet", event, params) { - data object ScreenOpened : CreateWallet("Create Wallet Screen Opened") - data object ButtonCreateWallet : CreateWallet("Button - Create Wallet") + class ScreenOpened : CreateWallet("Create Wallet Screen Opened") + class ButtonCreateWallet : CreateWallet("Button - Create Wallet") + class ButtonOtherOptions : CreateWallet("Button - Other Options") class WalletCreatedSuccessfully( creationType: WalletCreationType = WalletCreationType.PrivateKey, seedPhraseLength: Int? = null, + passPhraseState: AnalyticsParam.EmptyFull, ) : CreateWallet( event = "Wallet Created Successfully", params = buildMap { put("Creation Type", creationType.value) + put("Passphrase", passPhraseState.value) if (seedPhraseLength != null) { put("Seed Phrase Length", seedPhraseLength.toString()) } @@ -40,54 +42,39 @@ sealed class OnboardingEvent( } } - sealed class Topup( - event: String, - params: Map = mapOf(), - ) : OnboardingEvent("Onboarding / Top Up", event, params) { - - object ScreenOpened : Topup("Activation Screen Opened") - - object ButtonShowWalletAddress : Topup("Button - Show the Wallet Address") - - class ButtonBuyCrypto(currency: CryptoCurrency) : Topup( - event = "Button - Buy Crypto", - params = mapOf(AnalyticsParam.CURRENCY to currency.symbol), - ) - } - sealed class Backup( event: String, params: Map = mapOf(), ) : OnboardingEvent("Onboarding / Backup", event, params) { - data object ScreenOpened : Backup("Backup Screen Opened") - data object Started : Backup("Backup Started") - data object Skipped : Backup("Backup Skipped") - data object SettingAccessCodeStarted : Backup("Setting Access Code Started") - data object AccessCodeEntered : Backup("Access Code Entered") - data object AccessCodeReEntered : Backup("Access Code Re-entered") + 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"), ) - data object ResetCancelEvent : Backup( + class ResetCancelEvent : Backup( event = "Reset Card Notification", params = mapOf("Option" to "Cancel"), ) - data object ResetPerformEvent : Backup( + class ResetPerformEvent : Backup( event = "Reset Card Notification", params = mapOf("Option" to "Reset"), ) - data object ResumeInterruptedBackup : Backup( + class ResumeInterruptedBackup : Backup( event = "Notice - Backup Canceled", params = mapOf("Action" to "Resume"), ) - data object CancelInterruptedBackup : Backup( + class CancelInterruptedBackup : Backup( event = "Notice - Backup Canceled", params = mapOf("Action" to "Cancel"), ) @@ -98,9 +85,9 @@ sealed class OnboardingEvent( params: Map = mapOf(), ) : OnboardingEvent("Onboarding / Twins", event, params) { - data object ScreenOpened : Twins("Twinning Screen Opened") - data object SetupStarted : Twins("Twin Setup Started") - data object SetupFinished : Twins("Twin Setup Finished") + class ScreenOpened : Twins("Twinning Screen Opened") + class SetupStarted : Twins("Twin Setup Started") + class SetupFinished : Twins("Twin Setup Finished") } data class OfflineAttestationFailed( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt index 0827c50a61..6f3f8945e9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -157,14 +157,14 @@ internal class OnboardingEntryModel @Inject constructor( modelScope.launch { if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowAskBiometry()) { doIfVisa { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.BiometricScreenOpened) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.BiometricScreenOpened()) } stackNavigation.replaceAll( OnboardingRoute.AskBiometry(modelCallbacks = AskBiometryModelCallbacks(doneMode)), ) } else { doIfVisa { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened()) } stackNavigation.replaceAll( OnboardingRoute.Done( @@ -182,7 +182,7 @@ internal class OnboardingEntryModel @Inject constructor( override fun onAllowed() { analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.On)) doIfVisa { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened()) } stackNavigation.replaceAll( OnboardingRoute.Done( @@ -195,7 +195,7 @@ internal class OnboardingEntryModel @Inject constructor( override fun onDenied() { analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.Off)) doIfVisa { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened()) } stackNavigation.replaceAll( OnboardingRoute.Done( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt index f13142d4d5..6526071a17 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt @@ -209,7 +209,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor } Done -> { // final step - navigate to parent - analyticsHandler.send(OnboardingEvent.Finished) + analyticsHandler.send(OnboardingEvent.Finished()) val userWallet = childParams.multiWalletState.value.resultUserWallet ?: return params.onDone(userWallet) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt index a432deb8d0..cc9e104b95 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt @@ -37,7 +37,7 @@ internal class MultiWalletAccessCodeModel @Inject constructor( val onDismiss = MutableSharedFlow() init { - analyticsHandler.send(OnboardingEvent.Backup.SettingAccessCodeStarted) + analyticsHandler.send(OnboardingEvent.Backup.SettingAccessCodeStarted()) params.multiWalletState.update { it.copy(accessCode = null) @@ -112,7 +112,7 @@ internal class MultiWalletAccessCodeModel @Inject constructor( } } - analyticsHandler.send(OnboardingEvent.Backup.AccessCodeEntered) + analyticsHandler.send(OnboardingEvent.Backup.AccessCodeEntered()) } MultiWalletAccessCodeUM.Step.ConfirmAccessCode -> { if (checkAccessCode()) { @@ -125,7 +125,7 @@ internal class MultiWalletAccessCodeModel @Inject constructor( modelScope.launch { onDismiss.emit(Unit) } } - analyticsHandler.send(OnboardingEvent.Backup.AccessCodeReEntered) + analyticsHandler.send(OnboardingEvent.Backup.AccessCodeReEntered()) } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt index 4628feb506..d070a4ffed 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt @@ -80,10 +80,10 @@ class MultiWalletBackupModel @Inject constructor( init { // for wallet 1 this event is sent in Wallet1ChooseOptionModel if (scanResponse.productType == ProductType.Wallet2 || scanResponse.productType == ProductType.Ring) { - analyticsEventHandler.send(OnboardingEvent.Backup.ScreenOpened) + analyticsEventHandler.send(OnboardingEvent.Backup.ScreenOpened()) } - analyticsEventHandler.send(OnboardingEvent.Backup.Started) + analyticsEventHandler.send(OnboardingEvent.Backup.Started()) // Clear any saved backup before starting the backup process // also clears the primary card if it was set @@ -226,7 +226,7 @@ class MultiWalletBackupModel @Inject constructor( _uiState.update { it.copy(dialog = null) } }, onDismissClick = { - analyticsEventHandler.send(OnboardingEvent.Backup.ResetCancelEvent) + analyticsEventHandler.send(OnboardingEvent.Backup.ResetCancelEvent()) }, ), ) @@ -250,7 +250,7 @@ class MultiWalletBackupModel @Inject constructor( private fun showCardVerificationFailedDialog(error: TangemSdkError.CardVerificationFailed) { analyticsEventHandler.send( - event = OnboardingAnalyticsEvent.Onboarding.OfflineAttestationFailed(AnalyticsParam.ScreensSources.Backup), + event = OnboardingAnalyticsEvent.Error.OfflineAttestationFailed(AnalyticsParam.ScreensSources.Backup), ) val resource = error.localizedDescriptionRes() @@ -270,7 +270,7 @@ class MultiWalletBackupModel @Inject constructor( } private fun resetBackupCard(cardId: String) { - analyticsEventHandler.send(OnboardingEvent.Backup.ResetPerformEvent) + analyticsEventHandler.send(OnboardingEvent.Backup.ResetPerformEvent()) modelScope.launch { tangemSdkManager.resetToFactorySettings( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt index e17f81fcdb..8d508c9a4b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt @@ -35,7 +35,7 @@ internal class Wallet1ChooseOptionModel @Inject constructor( val returnToParentFlow = MutableSharedFlow() init { - analyticsHandler.send(OnboardingEvent.Backup.ScreenOpened) + analyticsHandler.send(OnboardingEvent.Backup.ScreenOpened()) } val canSkipBackup = params.multiWalletState.value.currentScanResponse.card.canSkipBackup @@ -44,7 +44,7 @@ internal class Wallet1ChooseOptionModel @Inject constructor( if (skipClicked) return skipClicked = true - analyticsHandler.send(OnboardingEvent.Backup.Skipped) + analyticsHandler.send(OnboardingEvent.Backup.Skipped()) modelScope.launch { val scanResponse = params.multiWalletState.value.currentScanResponse diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt index 1567ffe625..1cb37f4eb6 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -63,11 +64,12 @@ internal class MultiWalletCreateWalletModel @Inject constructor( resourceReference(R.string.onboarding_create_wallet_body) }, onCreateWalletClick = { - analyticsHandler.send(OnboardingEvent.CreateWallet.ButtonCreateWallet) + analyticsHandler.send(OnboardingEvent.CreateWallet.ButtonCreateWallet()) createWallet(false) }, showOtherOptionsButton = params.parentParams.withSeedPhraseFlow, onOtherOptionsClick = { + analyticsHandler.send(OnboardingEvent.CreateWallet.ButtonOtherOptions()) modelScope.launch { onDone.emit(Step.SeedPhrase) } @@ -80,7 +82,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor( val onDone = MutableSharedFlow() init { - analyticsHandler.send(OnboardingEvent.CreateWallet.ScreenOpened) + analyticsHandler.send(OnboardingEvent.CreateWallet.ScreenOpened()) } private fun createWallet(shouldReset: Boolean) { @@ -104,7 +106,11 @@ internal class MultiWalletCreateWalletModel @Inject constructor( cardRepository.startCardActivation(cardId = result.data.card.cardId) - analyticsHandler.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully()) + analyticsHandler.send( + event = OnboardingEvent.CreateWallet.WalletCreatedSuccessfully( + passPhraseState = AnalyticsParam.EmptyFull.Empty, + ), + ) val cardDoesNotSupportBackup = result.data.card.settings.isBackupAllowed.not() when { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/MultiWalletFinalizeComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/MultiWalletFinalizeComponent.kt index 824770dc83..5bb2626fe5 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/MultiWalletFinalizeComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/MultiWalletFinalizeComponent.kt @@ -36,6 +36,7 @@ internal class MultiWalletFinalizeComponent( private val resetCardsComponent = resetCardsComponentFactory.create( context = child("ResetCardsComponent"), params = ResetCardsComponent.Params( + source = ResetCardsComponent.Params.Source.Onboarding, callbacks = model.resetCardsModelCallbacks, ), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt index 7664244a73..4c9424cf98 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt @@ -41,6 +41,7 @@ import com.tangem.sdk.api.BackupServiceHolder import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.StringsSigns import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -91,7 +92,8 @@ internal class MultiWalletFinalizeModel @Inject constructor( // sets proper artwork state for initial step // (if we start from backup cards, we need to show proper artwork) ([REDACTED_TASK_KEY]) when (getInitialStep()) { - MultiWalletFinalizeUM.Step.Primary -> { /* state is already set */ } + MultiWalletFinalizeUM.Step.Primary -> { /* state is already set */ + } MultiWalletFinalizeUM.Step.BackupDevice1 -> { onEvent.emit(MultiWalletFinalizeComponent.Event.OneBackupCardAdded) } @@ -299,7 +301,11 @@ internal class MultiWalletFinalizeModel @Inject constructor( .updateWithHotWallet(wallet), ) }, - ).getOrElse { + ).onRight { + launch(NonCancellable) { + runSuspendCatching { walletsRepository.upgradeWallet(userWalletCreated.walletId) } + } + }.getOrElse { error("Failed to upgrade to cold wallet. Error: $it") } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt index 52538e1e92..0f18033dd3 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt @@ -5,6 +5,8 @@ import arrow.core.getOrElse import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -60,6 +62,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor( private val isWalletAlreadySavedUseCase: IsWalletAlreadySavedUseCase, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() @@ -108,6 +111,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor( updateUiState = { block -> updateUiStateSpecific(block) }, readyToImport = { ready -> state.update { it.copy(readyToImport = ready) } }, importWallet = { mnemonic, passphrase -> + analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ButtonImport()) importWallet( mnemonic = mnemonic, passphrase = passphrase, @@ -149,6 +153,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor( private fun getInitialUIState(): MultiWalletSeedPhraseUM { return MultiWalletSeedPhraseUM.Start( onImportSeedPhraseClicked = { + analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ButtonImportWallet()) openImportSeedPhrase() }, onGenerateSeedPhraseClicked = { @@ -177,6 +182,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor( } private fun openImportSeedPhrase() { + analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ImportSeedPhraseScreenOpened()) _uiState.value = importSeedPhraseUiStateBuilder.getState() } @@ -232,6 +238,11 @@ internal class MultiWalletSeedPhraseModel @Inject constructor( OnboardingEvent.CreateWallet.WalletCreationType.SeedImport }, seedPhraseLength = mnemonic.mnemonicComponents.size, + passPhraseState = if (passphrase.isNullOrBlank()) { + AnalyticsParam.EmptyFull.Empty + } else { + AnalyticsParam.EmptyFull.Full + }, ), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt index 9895662a0a..d254fca350 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt @@ -58,7 +58,7 @@ internal class OnboardingMultiWalletModel @Inject constructor( val uiState = _uiState.asStateFlow() init { - analyticsHandler.send(OnboardingEvent.Started) + analyticsHandler.send(OnboardingEvent.Started()) initScreenTitle() loadCardArtwork() subscribeToBackups() diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt index efecb19956..51db083121 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt @@ -1,7 +1,8 @@ package com.tangem.features.onboarding.v2.note.impl.child.create.model import com.tangem.common.CompletionResult -import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -22,6 +23,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class OnboardingNoteCreateWalletModel @Inject constructor( paramsContainer: ParamsContainer, @@ -30,6 +32,7 @@ internal class OnboardingNoteCreateWalletModel @Inject constructor( private val cardRepository: CardRepository, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, private val saveWalletUseCase: SaveWalletUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() @@ -41,11 +44,11 @@ internal class OnboardingNoteCreateWalletModel @Inject constructor( ) init { - Analytics.send(OnboardingEvent.CreateWallet.ScreenOpened) + analyticsEventHandler.send(OnboardingEvent.CreateWallet.ScreenOpened()) modelScope.launch { val scanResponse = params.childParams.commonState.value.scanResponse ?: return@launch if (!cardRepository.isActivationStarted(scanResponse.card.cardId)) { - Analytics.send(OnboardingEvent.Started) + analyticsEventHandler.send(OnboardingEvent.Started()) } } observeArtwork() @@ -62,7 +65,11 @@ internal class OnboardingNoteCreateWalletModel @Inject constructor( val result = tangemSdkManager.createProductWallet(scanResponse) when (result) { is CompletionResult.Success -> { - Analytics.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully()) + analyticsEventHandler.send( + event = OnboardingEvent.CreateWallet.WalletCreatedSuccessfully( + passPhraseState = AnalyticsParam.EmptyFull.Empty, + ), + ) createWalletAndNavigateBackWithDone(scanResponse.copy(card = result.data.card)) } is CompletionResult.Failure -> _uiState.update { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt index ddfd182953..04edacf8ef 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.note.impl.model import com.arkivanov.decompose.router.stack.StackNavigation import com.tangem.common.ui.userwallet.converter.ArtworkUMConverter +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -9,6 +10,7 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetCardImageUseCase +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.common.ui.exitOnboardingDialog import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.OnboardingNoteInnerNavigationState @@ -22,6 +24,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class OnboardingNoteModel @Inject constructor( paramsContainer: ParamsContainer, @@ -30,6 +33,7 @@ internal class OnboardingNoteModel @Inject constructor( private val messageSender: UiMessageSender, private val getCardImageUseCase: GetCardImageUseCase, private val artworkUMConverter: ArtworkUMConverter, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { @Suppress("UnusedPrivateMember") @@ -72,6 +76,7 @@ internal class OnboardingNoteModel @Inject constructor( } fun onWalletCreated(userWallet: UserWallet) { + analyticsEventHandler.send(OnboardingEvent.Finished()) commonUiState.update { it.copy(userWallet = userWallet) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index f1559c092d..e70843bd7b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -103,7 +103,7 @@ internal class OnboardingTwinModel @Inject constructor( init { when (_uiState.value) { is OnboardingTwinUM.Welcome -> { - analyticsEventHandler.send(OnboardingEvent.Twins.ScreenOpened) + analyticsEventHandler.send(OnboardingEvent.Twins.ScreenOpened()) modelScope.launch { saveTwinsOnboardingShownUseCase() } @@ -171,7 +171,11 @@ internal class OnboardingTwinModel @Inject constructor( } } - analyticsEventHandler.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully()) + analyticsEventHandler.send( + event = OnboardingEvent.CreateWallet.WalletCreatedSuccessfully( + passPhraseState = AnalyticsParam.EmptyFull.Empty, + ), + ) update { it.copy( @@ -272,7 +276,7 @@ internal class OnboardingTwinModel @Inject constructor( } private fun scanComplete(scanResponse: ScanResponse) { - analyticsEventHandler.send(OnboardingEvent.Twins.SetupFinished) + analyticsEventHandler.send(OnboardingEvent.Twins.SetupFinished()) when (params.mode) { Mode.WelcomeOnly -> saveWalletAndDone() diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/model/ResetCardsModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/model/ResetCardsModel.kt index 5a89688e94..20d87c7278 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/model/ResetCardsModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/util/impl/model/ResetCardsModel.kt @@ -1,6 +1,5 @@ package com.tangem.features.onboarding.v2.util.impl.model -import com.tangem.common.CompletionResult import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -8,12 +7,13 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.card.ResetCardUseCase +import com.tangem.domain.card.ResetCardUserCodeParams import com.tangem.domain.card.common.util.getBackupCardsCount import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.util.ResetCardsComponent -import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn @@ -26,7 +26,7 @@ import kotlin.coroutines.resume internal class ResetCardsModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, - private val tangemSdkManager: TangemSdkManager, + private val resetCardUseCase: ResetCardUseCase, private val uiMessageSender: UiMessageSender, ) : Model() { @@ -41,14 +41,19 @@ internal class ResetCardsModel @Inject constructor( } private suspend fun startFullResetFlow(createdUserWallet: UserWallet.Cold) { + val userCodeParams = ResetCardUserCodeParams( + isAccessCodeSet = createdUserWallet.scanResponse.card.isAccessCodeSet, + isPasscodeSet = createdUserWallet.scanResponse.card.isPasscodeSet, + ) + suspendCancellableCoroutine { continuation -> uiMessageSender.send( DialogMessage.invoke( title = resourceReference(R.string.reset_cards_dialog_first_title), - isDismissable = false, + isDismissable = true, message = resourceReference(R.string.reset_cards_dialog_first_description), firstActionBuilder = { - cancelAction { + cancelAction(isWarning = true) { callbacks.onCancel() onDismissRequest() continuation.resume(Unit) @@ -57,13 +62,13 @@ internal class ResetCardsModel @Inject constructor( secondActionBuilder = { EventMessageAction( title = resourceReference(R.string.card_settings_action_sheet_reset), - isWarning = true, + isWarning = false, onClick = { modelScope.launch { - repeatUntilTrue { resetPrimaryCard(createdUserWallet) } - continuation.resume(Unit) onDismissRequest() - startResetBackupCardsFlow(createdUserWallet) + resetOrCancel { resetPrimaryCard(createdUserWallet, userCodeParams) } + continuation.resume(Unit) + startResetBackupCardsFlow(createdUserWallet, userCodeParams) } }, ) @@ -74,7 +79,10 @@ internal class ResetCardsModel @Inject constructor( } } - private suspend fun startResetBackupCardsFlow(createdUserWallet: UserWallet.Cold) { + private suspend fun startResetBackupCardsFlow( + createdUserWallet: UserWallet.Cold, + userCodeParams: ResetCardUserCodeParams, + ) { val backupCardsCount = createdUserWallet.scanResponse.getBackupCardsCount() ?: 0 if (backupCardsCount == 0) { @@ -82,6 +90,8 @@ internal class ResetCardsModel @Inject constructor( return } + var canceled = false + repeat(backupCardsCount) { index -> suspendCancellableCoroutine { continuation -> uiMessageSender.send( @@ -90,23 +100,46 @@ internal class ResetCardsModel @Inject constructor( isDismissable = false, message = resourceReference(R.string.reset_cards_dialog_next_device_description), firstActionBuilder = { - cancelAction { - callbacks.onCancel() + cancelAction(isWarning = true) { onDismissRequest() - continuation.resume(Unit) + recommendResetAgain( + onCancel = { + canceled = true + callbacks.onCancel() + continuation.resume(Unit) + }, + onReset = onReset@{ + this@onReset.onDismissRequest() + + modelScope.launch { + resetOrCancel { + resetBackupCard( + cardNumber = index + 2, + params = userCodeParams, + userWalletId = createdUserWallet.walletId, + ) + } + continuation.resume(Unit) + } + }, + ) } }, secondActionBuilder = { EventMessageAction( title = resourceReference(R.string.card_settings_action_sheet_reset), - isWarning = true, onClick = { + onDismissRequest() + modelScope.launch { - repeatUntilTrue { resetBackupCard(index + 1, createdUserWallet.walletId) } - onDismissRequest() - if (index == backupCardsCount - 1) { - completeResetFlow() + resetOrCancel { + resetBackupCard( + cardNumber = index + 2, + params = userCodeParams, + userWalletId = createdUserWallet.walletId, + ) } + continuation.resume(Unit) } }, @@ -116,7 +149,35 @@ internal class ResetCardsModel @Inject constructor( ), ) } + + if (canceled) return } + + completeResetFlow() + } + + private fun recommendResetAgain(onCancel: () -> Unit, onReset: EventMessageAction.BuilderScope.() -> Unit) { + uiMessageSender.send( + DialogMessage.invoke( + title = resourceReference(R.string.card_reset_alert_incomplete_title), + isDismissable = false, + message = resourceReference(R.string.card_reset_alert_incomplete_message), + firstActionBuilder = { + cancelAction(isWarning = true) { + onCancel() + onDismissRequest() + } + }, + secondActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.card_settings_action_sheet_reset), + onClick = { + onReset() + }, + ) + }, + ), + ) } private suspend fun completeResetFlow() { @@ -125,10 +186,20 @@ internal class ResetCardsModel @Inject constructor( DialogMessage.invoke( title = resourceReference(R.string.card_settings_completed_reset_alert_title), isDismissable = false, - message = resourceReference(R.string.reset_cards_dialog_complete_description), + message = when (params.source) { + ResetCardsComponent.Params.Source.Upgrade -> + resourceReference(R.string.card_reset_alert_finish_message) + ResetCardsComponent.Params.Source.Onboarding -> + resourceReference(R.string.card_settings_completed_reset_alert_message) + }, firstActionBuilder = { EventMessageAction( - title = resourceReference(R.string.common_done), + title = when (params.source) { + ResetCardsComponent.Params.Source.Upgrade -> + resourceReference(R.string.card_reset_alert_finish_ok_button) + ResetCardsComponent.Params.Source.Onboarding -> + resourceReference(R.string.common_done) + }, onClick = { modelScope.launch { onDismissRequest() @@ -144,31 +215,47 @@ internal class ResetCardsModel @Inject constructor( } } - private suspend fun resetPrimaryCard(createdUserWallet: UserWallet.Cold): Boolean { - val scanResponse = createdUserWallet.scanResponse + private suspend fun resetOrCancel(action: suspend () -> Boolean) { + while (true) { + if (action()) { + return + } - val result = tangemSdkManager.resetToFactorySettings( - cardId = scanResponse.card.cardId, - allowsRequestAccessCodeFromRepository = true, + suspendCancellableCoroutine { continuation -> + recommendResetAgain( + onCancel = { + callbacks.onCancel() + continuation.cancel() + }, + onReset = { + continuation.resume(Unit) + }, + ) + } + } + } + + private suspend fun resetPrimaryCard( + createdUserWallet: UserWallet.Cold, + params: ResetCardUserCodeParams, + ): Boolean { + val result = resetCardUseCase.invoke( + cardId = createdUserWallet.scanResponse.card.cardId, + params = params, ) - return when (result) { - is CompletionResult.Failure -> false - is CompletionResult.Success -> true - } + return result.isRight() } - private suspend fun resetBackupCard(cardIndex: Int, userWalletId: UserWalletId): Boolean { - val result = tangemSdkManager.resetBackupCard(cardIndex, userWalletId) - return when (result) { - is CompletionResult.Failure -> false - is CompletionResult.Success -> true - } - } - - private inline fun repeatUntilTrue(action: () -> Boolean) { - while (true) { - if (action()) break - } + private suspend fun resetBackupCard( + cardNumber: Int, + params: ResetCardUserCodeParams, + userWalletId: UserWalletId, + ): Boolean { + return resetCardUseCase.invoke( + cardNumber = cardNumber, + params = params, + userWalletId = userWalletId, + ).getOrNull() == true } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt index b921e0641d..2d81af1b09 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt @@ -71,7 +71,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( val onDone = MutableSharedFlow() init { - analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.SettingAccessCodeStarted) + analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.SettingAccessCodeStarted()) } fun onBack() { @@ -115,14 +115,14 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( private fun onContinue() { when (uiState.value.step) { OnboardingVisaAccessCodeUM.Step.Enter -> { - analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.AccessCodeEntered) + analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.AccessCodeEntered()) if (checkAccessCodeMinChars().not()) return _uiState.update { it.copy(step = OnboardingVisaAccessCodeUM.Step.ReEnter) } - analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.AccessCodeReenterScreen) + analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.AccessCodeReenterScreen()) } OnboardingVisaAccessCodeUM.Step.ReEnter -> { if (checkAccessCodesMatch().not()) return - analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.OnboardingVisa) + analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.OnboardingVisa()) startActivationProcess(accessCode = uiState.value.accessCodeFirst.text) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt index ba23fac765..cf38445872 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt @@ -55,7 +55,7 @@ internal class OnboardingVisaApproveModel @Inject constructor( val onDone = MutableSharedFlow() init { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.WalletPrepare) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.WalletPrepare()) } private fun getInitialState(): OnboardingVisaApproveUM { @@ -67,7 +67,7 @@ internal class OnboardingVisaApproveModel @Inject constructor( private fun onApproveClick() { loading(true) - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonApprove) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonApprove()) modelScope.launch { val dataToSign = diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt index 3e7fbaa01c..235c201961 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt @@ -61,7 +61,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( val onDone = MutableSharedFlow() init { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ActivationInProgressScreen) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ActivationInProgressScreen()) runShortPolling() } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt index 13e71e07f0..ace03f9a08 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt @@ -52,7 +52,7 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor( val onDone = MutableSharedFlow() init { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.GoToWebsiteOpened) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.GoToWebsiteOpened()) modelScope.launch { while (true) { visaActivationRepository.getActivationRemoteState() @@ -81,12 +81,12 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor( } private fun onShareClicked() { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonShareLink) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonShareLink()) shareManager.shareText("https://tangem.com/") // TODO } private fun onOpenInBrowserClicked() { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonBrowser) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonBrowser()) urlOpener.openUrl("https://tangem.com/") // TODO } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt index 5ba1dd3aee..ae3bf7d96e 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt @@ -47,7 +47,7 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( val onDone = MutableSharedFlow() init { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.PinCodeScreenOpened) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.PinCodeScreenOpened()) } private fun getInitialState(): OnboardingVisaPinCodeUM { @@ -67,12 +67,12 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( if (PinCodeValidation.validateAllDigits(pin)) { val isError = PinCodeValidation.validateLength(pin) && PinCodeValidation.validate(pin).not() - _uiState.update { - it.copy( + _uiState.update { currentState -> + currentState.copy( pinCode = pin, submitButtonEnabled = PinCodeValidation.validate(pin), error = if (isError) { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ErrorPinValidation) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ErrorPinValidation()) resourceReference(R.string.visa_onboarding_pin_validation_error_message) } else { null @@ -83,7 +83,7 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( } private fun onSubmitClick() { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.PinEntered) + analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.PinEntered()) val pinCode = _uiState.value.pinCode if (PinCodeValidation.validate(pinCode).not()) return diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt index 9e63e68fc3..8660ff65d0 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt @@ -50,7 +50,7 @@ internal class OnboardingVisaWelcomeModel @Inject constructor( val onDone = MutableSharedFlow() init { - analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.ActivationScreenOpened) + analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.ActivationScreenOpened()) } private fun getInitialState(): OnboardingVisaWelcomeUM { @@ -65,7 +65,7 @@ internal class OnboardingVisaWelcomeModel @Inject constructor( } private fun onContinueClick() { - analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.ButtonActivate) + analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.ButtonActivate()) if (params !is Config.WelcomeBack) { modelScope.launch { onDone.emit(DoneEvent.WelcomeDone) } return diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/analytics/OnboardingVisaAnalyticsEvent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/analytics/OnboardingVisaAnalyticsEvent.kt index 25759dce3a..f2abbfba47 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/analytics/OnboardingVisaAnalyticsEvent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/analytics/OnboardingVisaAnalyticsEvent.kt @@ -7,27 +7,27 @@ internal sealed class OnboardingVisaAnalyticsEvent( params: Map = mapOf(), ) : AnalyticsEvent("Onboarding / Visa", event, params) { - data object ActivationScreenOpened : OnboardingVisaAnalyticsEvent( + class ActivationScreenOpened : OnboardingVisaAnalyticsEvent( event = "Activation Screen Opened", ) - data object ButtonActivate : OnboardingVisaAnalyticsEvent( + class ButtonActivate : OnboardingVisaAnalyticsEvent( event = "Button - Activate", ) - data object SettingAccessCodeStarted : OnboardingVisaAnalyticsEvent( + class SettingAccessCodeStarted : OnboardingVisaAnalyticsEvent( event = "Setting Access Code Started", ) - data object AccessCodeEntered : OnboardingVisaAnalyticsEvent( + class AccessCodeEntered : OnboardingVisaAnalyticsEvent( event = "Access Code Entered", ) - data object AccessCodeReenterScreen : OnboardingVisaAnalyticsEvent( + class AccessCodeReenterScreen : OnboardingVisaAnalyticsEvent( event = "Access Code Re-enter Screen", ) - data object OnboardingVisa : OnboardingVisaAnalyticsEvent( + class OnboardingVisa : OnboardingVisaAnalyticsEvent( event = "Onboarding / Visa", ) @@ -38,48 +38,48 @@ internal sealed class OnboardingVisaAnalyticsEvent( params = mapOf("Type" to type), ) - data object WalletPrepare : OnboardingVisaAnalyticsEvent( + class WalletPrepare : OnboardingVisaAnalyticsEvent( event = "Wallet Prepare", ) - data object ButtonApprove : OnboardingVisaAnalyticsEvent( + class ButtonApprove : OnboardingVisaAnalyticsEvent( event = "Button - Approve", ) - data object GoToWebsiteOpened : OnboardingVisaAnalyticsEvent( + class GoToWebsiteOpened : OnboardingVisaAnalyticsEvent( event = "Go To Website Opened", ) - data object ButtonBrowser : OnboardingVisaAnalyticsEvent( + class ButtonBrowser : OnboardingVisaAnalyticsEvent( event = "Button - Browser", ) - data object ButtonShareLink : OnboardingVisaAnalyticsEvent( + class ButtonShareLink : OnboardingVisaAnalyticsEvent( event = "Button - Share Link", ) - data object ActivationInProgressScreen : OnboardingVisaAnalyticsEvent( + class ActivationInProgressScreen : OnboardingVisaAnalyticsEvent( event = "Activation In Progress Screen", ) - data object PinCodeScreenOpened : OnboardingVisaAnalyticsEvent( + class PinCodeScreenOpened : OnboardingVisaAnalyticsEvent( event = "PIN Code Screen Opened", ) - data object PinEntered : OnboardingVisaAnalyticsEvent( + class PinEntered : OnboardingVisaAnalyticsEvent( event = "PIN Entered", params = mapOf("Type" to "Visa"), ) - data object BiometricScreenOpened : OnboardingVisaAnalyticsEvent( + class BiometricScreenOpened : OnboardingVisaAnalyticsEvent( event = "Biometric Screen Opened", ) - data object SuccessScreenOpened : OnboardingVisaAnalyticsEvent( + class SuccessScreenOpened : OnboardingVisaAnalyticsEvent( event = "Success Screen Opened", ) - data object ErrorPinValidation : OnboardingVisaAnalyticsEvent( + class ErrorPinValidation : OnboardingVisaAnalyticsEvent( event = "Error - Pin Validation", ) } \ No newline at end of file diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index 44736bee00..290dd27ce5 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -48,6 +48,8 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.transaction.models) implementation(projects.domain.account.status) + implementation(projects.domain.appTheme) + implementation(projects.domain.appTheme.models) /** DI */ implementation(deps.hilt.android) diff --git a/features/onramp/impl/detekt-baseline-debug.xml b/features/onramp/impl/detekt-baseline-debug.xml index 4cc1802ed8..ecf2e0cce8 100644 --- a/features/onramp/impl/detekt-baseline-debug.xml +++ b/features/onramp/impl/detekt-baseline-debug.xml @@ -1,13 +1,5 @@ - - NonBooleanPropertyPrefixedWithIs:AvailableSwapPairsModel.kt$AvailableSwapPairsModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:HotCryptoModel.kt$HotCryptoModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:OnrampAddTokenUiBuilder.kt$OnrampAddTokenUiBuilder$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:OnrampMainComponentModel.kt$OnrampMainComponentModel$private val isDemoCardUseCase: IsDemoCardUseCase - NonBooleanPropertyPrefixedWithIs:OnrampOperationModel.kt$OnrampOperationModel$private val isDemoCardUseCase: IsDemoCardUseCase - NonBooleanPropertyPrefixedWithIs:OnrampTokenListModel.kt$OnrampTokenListModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:SwapSelectTokensModel.kt$SwapSelectTokensModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - + diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt index 6035f80287..4a29045354 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt @@ -86,7 +86,7 @@ internal class AllOffersStateFactory( fun getPaymentsState(): AllOffersStateUM { return when (val currentState = currentStateProvider.invoke()) { is AllOffersStateUM.Content -> { - analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened) + analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened()) currentState.copy(currentMethod = null) } AllOffersStateUM.Loading, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt index cf2c6af946..b29663f8cc 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt @@ -48,8 +48,8 @@ internal class AllOffersModel @Inject constructor( init { subscribeOnAllOffers() - analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened) - analyticsEventHandler.send(OnrampAnalyticsEvent.AllOffersClicked) + analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened()) + analyticsEventHandler.send(OnrampAnalyticsEvent.AllOffersClicked()) } fun dismiss() { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt index 989bebc335..e2d8f30195 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt @@ -69,7 +69,7 @@ internal class ConfirmResidencyModel @Inject constructor( } else { ConfirmResidencyUM.ActionButtonConfig( onClick = { - analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp) + analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp()) router.pop() }, text = resourceReference(R.string.common_close), @@ -77,7 +77,7 @@ internal class ConfirmResidencyModel @Inject constructor( } private fun onChangeClick() { - analyticsEventHandler.send(OnrampAnalyticsEvent.OnResidenceChange) + analyticsEventHandler.send(OnrampAnalyticsEvent.OnResidenceChange()) bottomSheetNavigation.activate(ConfirmResidencyBottomSheetConfig.SelectCountry(params.onDismiss)) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt index b3569cec78..24010b69e9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt @@ -240,11 +240,11 @@ internal class OnrampAmountStateFactory( val errorTextRes = when (error) { is OnrampError.AmountError.TooBigError -> { - analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError) + analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError()) R.string.onramp_max_amount_restriction } is OnrampError.AmountError.TooSmallError -> { - analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError) + analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError()) R.string.onramp_min_amount_restriction } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 18b31bb447..7c501be535 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -288,7 +288,7 @@ internal class OnrampMainComponentModel @Inject constructor( } override fun openCurrenciesList() { - analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened) + analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened()) bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.CurrenciesList) } @@ -333,7 +333,7 @@ internal class OnrampMainComponentModel @Inject constructor( } private fun onCloseClick() { - analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp) + analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp()) router.pop() } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt index 5e4d4022dd..a91f463964 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt @@ -118,11 +118,11 @@ internal class OnrampV2AmountStateFactory( val errorTextRes = when (error) { is OnrampError.AmountError.TooBigError -> { - analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError) + analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError()) R.string.onramp_max_amount_restriction } is OnrampError.AmountError.TooSmallError -> { - analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError) + analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError()) R.string.onramp_min_amount_restriction } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt index 4efed551f6..517c304fd6 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt @@ -124,7 +124,7 @@ internal class OnrampV2MainComponentModel @Inject constructor( } override fun openCurrenciesList() { - analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened) + analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened()) bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.CurrenciesList) } @@ -225,7 +225,7 @@ internal class OnrampV2MainComponentModel @Inject constructor( } private fun onCloseClick() { - analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp) + analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp()) router.pop() } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt index 7c5493ae04..95cab56383 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt @@ -66,7 +66,7 @@ internal class SelectProviderModel @Inject constructor( userCountry = getUserCountryUseCase.invokeSync().getOrNull() ?: UserCountry.Other(Locale.getDefault().country) - analyticsEventHandler.send(OnrampAnalyticsEvent.ProvidersScreenOpened) + analyticsEventHandler.send(OnrampAnalyticsEvent.ProvidersScreenOpened()) getPaymentMethods() getProviders(params.selectedPaymentMethod) } @@ -140,7 +140,7 @@ internal class SelectProviderModel @Inject constructor( } private fun openPaymentMethods() { - analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened) + analyticsEventHandler.send(OnrampAnalyticsEvent.PaymentMethodsScreenOpened()) bottomSheetNavigation.activate( ProviderListBottomSheetConfig.PaymentMethods( selectedMethodId = state.value.selectedPaymentMethod.paymentMethod.id, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt index 3f0059348b..cd879af404 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt @@ -42,9 +42,9 @@ internal class DefaultOnrampRedirectComponent @AssistedInject constructor( BackHandler(onBack = params.onBack) OnrampRedirectContent(modifier = modifier, state = model.state) - val isDarkTheme = isSystemInDarkTheme() + val isSystemInDarkTheme = isSystemInDarkTheme() LaunchedEffect(model.state) { - model.getRedirectUrl(isDarkTheme = isDarkTheme) + model.getRedirectUrl(isSystemInDarkTheme = isSystemInDarkTheme) } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt index c803a99746..a550929c72 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.onramp.redirect.model +import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -13,6 +14,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.apptheme.GetAppThemeModeUseCase +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.onramp.GetOnrampRedirectUrlUseCase import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.model.error.OnrampError @@ -24,7 +27,9 @@ import com.tangem.features.onramp.redirect.entity.OnrampRedirectUM import com.tangem.features.onramp.success.OnrampSuccessScreenListener import com.tangem.features.onramp.utils.sendOnrampErrorEvent import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber @@ -38,6 +43,7 @@ internal class OnrampRedirectModel @Inject constructor( private val messageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, private val onrampSuccessScreenListener: OnrampSuccessScreenListener, + private val getAppThemeModeUseCase: GetAppThemeModeUseCase, private val appRouter: AppRouter, paramsContainer: ParamsContainer, getWalletsUseCase: GetWalletsUseCase, @@ -74,8 +80,17 @@ internal class OnrampRedirectModel @Inject constructor( subscribeToOnrampSuccessListener() } - fun getRedirectUrl(isDarkTheme: Boolean) { + fun getRedirectUrl(isSystemInDarkTheme: Boolean) { modelScope.launch { + val isDarkTheme = getAppThemeModeUseCase() + .map { result -> + when (result.getOrElse { AppThemeMode.FOLLOW_SYSTEM }) { + AppThemeMode.FORCE_DARK -> true + AppThemeMode.FORCE_LIGHT -> false + AppThemeMode.FOLLOW_SYSTEM -> isSystemInDarkTheme + } + }.firstOrNull() ?: isSystemInDarkTheme + getOnrampRedirectUrlUseCase.invoke( userWallet = selectedUserWallet, quote = params.onrampProviderWithQuote, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt index 8c22aeb96a..a383d11e9a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt @@ -60,7 +60,7 @@ internal class OnrampSelectCountryModel @Inject constructor( val state: StateFlow get() = controller.state init { - analyticsEventHandler.send(OnrampAnalyticsEvent.SelectResidenceOpened) + analyticsEventHandler.send(OnrampAnalyticsEvent.SelectResidenceOpened()) updateCountriesList() modelScope.launch { subscribeOnUpdateState() } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt index ce550dbc64..1c618de087 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt @@ -48,19 +48,20 @@ internal class OnrampOperationModel @Inject constructor( private val messageSender: UiMessageSender, private val rampStateManager: RampStateManager, ) : Model() { - private val params: Params = paramsContainer.require() - private val selectedUserWallet = getWalletsUseCase.invokeSync() - .first { it.walletId == params.userWalletId } + private val params: Params = paramsContainer.require() val state: StateFlow field = MutableStateFlow(value = getInitialState()) + private val selectedUserWallet = getWalletsUseCase.invokeSync() + .first { it.walletId == params.userWalletId } + init { analyticsEventHandler.send( event = when (params) { - is Params.Buy -> MainScreenAnalyticsEvent.BuyScreenOpened - is Params.Sell -> MainScreenAnalyticsEvent.SellScreenOpened + is Params.Buy -> MainScreenAnalyticsEvent.BuyScreenOpened() + is Params.Sell -> MainScreenAnalyticsEvent.SellScreenOpened() }, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/model/OnrampSettingsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/model/OnrampSettingsModel.kt index e7eba0a83f..7c6d7899b8 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/model/OnrampSettingsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/model/OnrampSettingsModel.kt @@ -37,7 +37,7 @@ internal class OnrampSettingsModel @Inject constructor( val bottomSheetNavigation: SlotNavigation = SlotNavigation() init { - analyticsEventHandler.send(OnrampAnalyticsEvent.SettingsOpened) + analyticsEventHandler.send(OnrampAnalyticsEvent.SettingsOpened()) subscribeOnUpdateState() } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt index ad9f3c8d31..5f318b6a4f 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt @@ -74,7 +74,7 @@ internal class OnrampSuccessComponentModel @Inject constructor( } override fun goToProviderClick(providerLink: String) { - analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) + analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider()) urlOpener.openUrl(providerLink) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt index 5040c07c57..d9b66c559d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt @@ -50,7 +50,7 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor( ) init { - analyticsEventHandler.send(event = MainScreenAnalyticsEvent.SwapScreenOpened) + analyticsEventHandler.send(event = MainScreenAnalyticsEvent.SwapScreenOpened()) } @Composable diff --git a/features/referral/domain/detekt-baseline-debug.xml b/features/referral/domain/detekt-baseline-debug.xml deleted file mode 100644 index 822bbfee9f..0000000000 --- a/features/referral/domain/detekt-baseline-debug.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - MultilineLambdaItParameter:ReferralInteractorImpl.kt$ReferralInteractorImpl${ Timber.e("Failed to derive public keys: $it") throw it.mapToDomainError() } - ObjectExtendsThrowable:ReferralError.kt$ReferralError$SdkError : ReferralError - ObjectExtendsThrowable:ReferralError.kt$ReferralError$UserCancelledException : ReferralError - UselessCallOnNotNull:ReferralInteractorImpl.kt$ReferralInteractorImpl$listOfNotNull(cryptoCurrency) - - diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 78d092c2da..02ddfaaf01 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -72,9 +72,9 @@ internal class ReferralInteractorImpl( manageCryptoCurrenciesUseCase(accountId = portfolioId.accountId, add = cryptoCurrency) } is PortfolioId.Wallet -> { - derivePublicKeysUseCase(userWallet.walletId, listOfNotNull(cryptoCurrency)).getOrElse { - Timber.e("Failed to derive public keys: $it") - throw it.mapToDomainError() + derivePublicKeysUseCase(userWallet.walletId, listOf(cryptoCurrency)).getOrElse { throwable -> + Timber.e("Failed to derive public keys: $throwable") + throw throwable.mapToDomainError() } addCryptoCurrenciesUseCase( @@ -118,9 +118,9 @@ internal class ReferralInteractorImpl( private fun Throwable.mapToDomainError(): ReferralError { if (this !is TangemSdkError) return ReferralError.DataError(this) return if (this is TangemSdkError.UserCancelled) { - ReferralError.UserCancelledException + ReferralError.UserCancelledException() } else { - ReferralError.SdkError + ReferralError.SdkError() } } } \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/errors/ReferralError.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/errors/ReferralError.kt index 8d78d31fdd..05e5eac920 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/errors/ReferralError.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/errors/ReferralError.kt @@ -1,8 +1,8 @@ package com.tangem.feature.referral.domain.errors sealed class ReferralError : Exception() { - data object UserCancelledException : ReferralError() - data object SdkError : ReferralError() + class UserCancelledException : ReferralError() + class SdkError : ReferralError() - data class DataError(val throwable: Throwable) : ReferralError() + class DataError(val throwable: Throwable) : ReferralError() } \ No newline at end of file diff --git a/features/referral/impl/detekt-baseline-debug.xml b/features/referral/impl/detekt-baseline-debug.xml index d0a4690a1f..9f594c6c2e 100644 --- a/features/referral/impl/detekt-baseline-debug.xml +++ b/features/referral/impl/detekt-baseline-debug.xml @@ -3,12 +3,9 @@ MultilineLambdaItParameter:AgreementText.kt${ val clickableSpanStyle = requireNotNull(agreementText.spanStyles.getOrNull(1)) if (it in clickableSpanStyle.start..clickableSpanStyle.end) { onClick() } } - MultilineLambdaItParameter:ReferralModel.kt$ReferralModel${ analyticsEventHandler.send(ReferralEvents.ParticipateSuccessful) referralData.value = it } MultilineLambdaItParameter:ReferralScreen.kt${ // TODO: use StateEvent if (stateHolder.errorSnackbar != null) { TangemSnackbar(data = it, actionOnNewLine = true) } else { CopiedTextSnackbar(it) } } MultilineLambdaItParameter:ReferralScreen.kt${ ReferralContent( stateHolder = stateHolder, snackbarHostState = snackbarHostState, onAgreementClick = stateHolder.analytics.onAgreementClicked, modifier = Modifier.padding(it), ) } NamedArguments:ReferralScreen.kt$Text( formatAwardConditionsString( quantity = award, network = networkName, address = if (!address.isNullOrBlank()) " $address" else "", ), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, modifier = Modifier.testTag(ReferralProgramScreenTestTags.INFO_FOR_YOU_TEXT), ) - NonBooleanPropertyPrefixedWithIs:ParticipateBottomBlock.kt$val isExpanded = remember { mutableStateOf(false) } - NonBooleanPropertyPrefixedWithIs:ReferralModel.kt$ReferralModel$private val isDemoCardUseCase: IsDemoCardUseCase SuspendFunSwallowedCancellation:ReferralModel.kt$ReferralModel$runCatching UseOrEmpty:ReferralScreen.kt$matchResult.groups[1]?.value ?: "" VarCouldBeVal:ReferralModel.kt$ReferralModel$private var referralData: MutableStateFlow<ReferralData?> = MutableStateFlow(null) diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt index 20a2fe5ecd..364b76ce0f 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt @@ -4,12 +4,18 @@ import com.tangem.core.analytics.models.AnalyticsEvent sealed class ReferralEvents(event: String) : AnalyticsEvent(REFERRAL_PROGRAM_CATEGORY, event) { - data object ReferralScreenOpened : ReferralEvents(event = "Referral Screen Opened") - data object ClickParticipate : ReferralEvents(event = "Button - Participate") - data object ClickCopy : ReferralEvents(event = "Button - Copy") - data object ClickShare : ReferralEvents(event = "Button - Share") - data object ClickTaC : ReferralEvents(event = "Link - TaC") - data object ParticipateSuccessful : ReferralEvents(event = "Participate Successful") + class ReferralScreenOpened : ReferralEvents(event = "Referral Screen Opened") + class ClickParticipate : ReferralEvents(event = "Button - Participate") + class ClickCopy : ReferralEvents(event = "Button - Copy") + class ClickShare : ReferralEvents(event = "Button - Share") + class ClickTaC : ReferralEvents(event = "Link - TaC") + class ParticipateSuccessful : ReferralEvents(event = "Participate Successful") } -private const val REFERRAL_PROGRAM_CATEGORY = "Referral Program" \ No newline at end of file +sealed class ReferralEventsAccounts(event: String) : AnalyticsEvent(REFERRAL_PROGRAM_ACCOUNT_CATEGORY, event) { + + class ListChooseAccount : ReferralEventsAccounts(event = "List - choose account") +} + +private const val REFERRAL_PROGRAM_CATEGORY = "Referral Program" +private const val REFERRAL_PROGRAM_ACCOUNT_CATEGORY = "Referral program - Account" \ No newline at end of file diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt index fd6d531d4f..46ce302791 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt @@ -30,6 +30,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.referral.analytics.ReferralEvents +import com.tangem.feature.referral.analytics.ReferralEventsAccounts import com.tangem.feature.referral.api.ReferralComponent import com.tangem.feature.referral.domain.ReferralInteractor import com.tangem.feature.referral.domain.errors.ReferralError @@ -90,7 +91,7 @@ internal class ReferralModel @Inject constructor( } init { - analyticsEventHandler.send(ReferralEvents.ReferralScreenOpened) + analyticsEventHandler.send(ReferralEvents.ReferralScreenOpened()) if (accountsFeatureToggles.isFeatureEnabled) { combine( flow = referralData.filterNotNull().onEach(::selectAccount), @@ -116,6 +117,7 @@ internal class ReferralModel @Inject constructor( private fun combineAccountUI(referralData: ReferralData): Flow = combine( flow = portfolioSelectorController.selectedAccountWithData(portfolioFetcher) + .onEach { analyticsEventHandler.send(ReferralEventsAccounts.ListChooseAccount()) } .onEach { bottomSheetNavigation.dismiss() }, flow2 = getBalanceHidingSettingsUseCase.isBalanceHidden(), flow3 = getSelectedAppCurrencyUseCase.invokeOrDefault(), @@ -178,7 +180,7 @@ internal class ReferralModel @Inject constructor( if (userWallet is UserWallet.Cold && isDemoCardUseCase(cardId = userWallet.cardId)) { showErrorSnackbar(DemoModeException()) } else { - analyticsEventHandler.send(ReferralEvents.ClickParticipate) + analyticsEventHandler.send(ReferralEvents.ClickParticipate()) val lastInfoState = uiState.referralInfoState uiState = uiState.copy(referralInfoState = ReferralInfoState.Loading) modelScope.launch { @@ -187,9 +189,9 @@ internal class ReferralModel @Inject constructor( false -> PortfolioId(params.userWalletId) } runCatching { referralInteractor.startReferral(portfolioId) } - .onSuccess { - analyticsEventHandler.send(ReferralEvents.ParticipateSuccessful) - referralData.value = it + .onSuccess { referral -> + analyticsEventHandler.send(ReferralEvents.ParticipateSuccessful()) + referralData.value = referral } .onFailure { throwable -> if (throwable is ReferralError.UserCancelledException) { @@ -213,17 +215,17 @@ internal class ReferralModel @Inject constructor( } private fun onAgreementClicked() { - analyticsEventHandler.send(ReferralEvents.ClickTaC) + analyticsEventHandler.send(ReferralEvents.ClickTaC()) lastReferralData?.tosLink?.let(urlOpener::openUrl) } private fun onCopyClicked() { - analyticsEventHandler.send(ReferralEvents.ClickCopy) + analyticsEventHandler.send(ReferralEvents.ClickCopy()) } private fun onShareClicked(text: String) { - analyticsEventHandler.send(ReferralEvents.ClickShare) + analyticsEventHandler.send(ReferralEvents.ClickShare()) shareManager.shareText(text = text) } diff --git a/features/send-v2/api/detekt-baseline-debug.xml b/features/send-v2/api/detekt-baseline-debug.xml deleted file mode 100644 index 6512d3e5ba..0000000000 --- a/features/send-v2/api/detekt-baseline-debug.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - BooleanPropertyNaming:FeeSelectorData.kt$FeeSelectorData$val removeSuggestedFee: Boolean = false - BooleanPropertyNaming:SendEntryRoute.kt$SendEntryRoute.ChooseToken$val showSendViaSwapNotification: Boolean - NonBooleanPropertyPrefixedWithIs:SendDestinationComponentParams.kt$SendDestinationComponentParams.DestinationParams$val isBalanceHidingFlow: StateFlow<Boolean> - UseEmptyCounterpart:CommonSendAmountAnalyticEvents.kt$CommonSendAmountAnalyticEvents$mapOf() - UseEmptyCounterpart:CommonSendAnalyticEvents.kt$CommonSendAnalyticEvents$mapOf() - UseEmptyCounterpart:CommonSendFeeAnalyticEvents.kt$CommonSendFeeAnalyticEvents$mapOf() - - diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt index 799c657659..82527c66bf 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt @@ -12,7 +12,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM sealed class CommonSendAnalyticEvents( category: String, event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = category, event = event, params = params) { /** Recipient address screen opened */ @@ -51,16 +51,29 @@ sealed class CommonSendAnalyticEvents( ), ) + @Suppress("NullableToStringCall") /** Confirmation screen opened */ data class ConfirmationScreenOpened( val categoryName: String, val source: CommonSendSource, + val fromDerivationIndex: Int?, + val toDerivationIndex: Int?, + val sendBlockchain: String, + val sendToken: String, ) : CommonSendAnalyticEvents( category = categoryName, event = "Confirm Screen Opened", - params = mapOf( - SOURCE to source.analyticsName, - ), + params = buildMap { + put(SOURCE, source.analyticsName) + put("Token", sendToken) + put("Blockchain", sendBlockchain) + if (fromDerivationIndex != null || toDerivationIndex != null) { + put( + "Account Derivation From or To (optional)", + "$fromDerivationIndex, $toDerivationIndex", + ) + } + }, ) /** If transaction delays notification is present */ diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entry/SendEntryRoute.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entry/SendEntryRoute.kt index 2fab40cdc0..ae701c2f99 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entry/SendEntryRoute.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entry/SendEntryRoute.kt @@ -13,6 +13,6 @@ sealed class SendEntryRoute : Route { data object SendWithSwap : SendEntryRoute() /** Route to choose token screen for send via swap */ data class ChooseToken( - val showSendViaSwapNotification: Boolean, + val isShowSendViaSwapNotification: Boolean, ) : SendEntryRoute() } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/amount/analytics/CommonSendAmountAnalyticEvents.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/amount/analytics/CommonSendAmountAnalyticEvents.kt index 5cc038b0cb..cfc6409387 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/amount/analytics/CommonSendAmountAnalyticEvents.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/amount/analytics/CommonSendAmountAnalyticEvents.kt @@ -10,7 +10,7 @@ import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents sealed class CommonSendAmountAnalyticEvents( category: String, event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = category, event = event, params = params) { /** Selected currency */ diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/analytics/CommonSendFeeAnalyticEvents.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/analytics/CommonSendFeeAnalyticEvents.kt index 500e8b4f69..7294b5e0b8 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/analytics/CommonSendFeeAnalyticEvents.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/analytics/CommonSendFeeAnalyticEvents.kt @@ -9,7 +9,7 @@ import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.Common sealed class CommonSendFeeAnalyticEvents( category: String, event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = category, event = event, params = params) { abstract val categoryName: String diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/entity/FeeSelectorData.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/entity/FeeSelectorData.kt index 66fa5e4156..be6f6de2bb 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/entity/FeeSelectorData.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/entity/FeeSelectorData.kt @@ -1,5 +1,5 @@ package com.tangem.features.send.v2.api.subcomponents.feeSelector.entity data class FeeSelectorData( - val removeSuggestedFee: Boolean = false, + val isRemoveSuggestedFee: Boolean = false, ) \ No newline at end of file diff --git a/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt b/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt index 7cfd18cb02..efcc805d31 100644 --- a/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt +++ b/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt @@ -168,7 +168,7 @@ class FeeCalculationUtilsTest { fiatAmount = BigDecimal.ZERO, fiatRate = BigDecimal.ZERO, priceChange = BigDecimal.ZERO, - yieldBalance = null, + stakingBalance = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), networkAddress = mockk(relaxed = true), diff --git a/features/send-v2/impl/detekt-baseline-debug.xml b/features/send-v2/impl/detekt-baseline-debug.xml deleted file mode 100644 index c88748c497..0000000000 --- a/features/send-v2/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,86 +0,0 @@ - - - - - BooleanPropertyNaming:ConfirmUM.kt$ConfirmUM.Content$val showTapHelp: Boolean - BooleanPropertyNaming:FeeSelectorAlertFactory.kt$FeeSelectorAlertFactory$val showFeeTooHigh = checkAndShowFeeTooHigh(feeSelectorUM, onConfirmClick) - BooleanPropertyNaming:FeeSelectorAlertFactory.kt$FeeSelectorAlertFactory$val showFeeTooLow = checkAndShowFeeTooLow(feeSelectorUM, onConfirmClick) - BooleanPropertyNaming:FeeSelectorModalBottomSheet.kt$val lastItem = index == state.feeItems.size - 1 - BooleanPropertyNaming:FeeSelectorModalBottomSheet.kt$val showDivider = index != customFeeFields.size - 1 || nonce is FeeNonce.Nonce - BooleanPropertyNaming:NFTSendAnalyticEvents.kt$NFTSendAnalyticEvents.TransactionScreenOpened$val nonceNotEmpty: Boolean - BooleanPropertyNaming:NFTSendSuccessContent.kt$var visible by remember { mutableStateOf(false) } - BooleanPropertyNaming:SendAnalyticEvents.kt$SendAnalyticEvents.TransactionScreenOpened$val nonceNotEmpty: Boolean - BooleanPropertyNaming:SendAnalyticHelper.kt$SendAnalyticHelper$val blockchainAddressForEns = (sendUM.destinationUM as? DestinationUM.Content)?.addressTextField?.isAddressEns - BooleanPropertyNaming:SendConfirmSuccessContent.kt$var visible by remember { mutableStateOf(false) } - BooleanPropertyNaming:SendEntryPointModel.kt$SendEntryPointModel$val showSendViaSwapNotification = shouldShowNotificationUseCase( NotificationId.SendViaSwapTokenSelectorNotification.key, ) - BooleanPropertyNaming:SendRecipientHistoryListConverter.kt$SendRecipientHistoryListConverter$val notZero = !item.amount.isZero() - BooleanPropertyNaming:TapHelp.kt$var wrappedIsDisplay by remember { mutableStateOf(false) } - CanBeNonNullable:SendDestinationContent.kt$memoField: DestinationTextFieldUM.RecipientMemo? - CanBeNonNullable:SendDestinationModel.kt$SendDestinationModel$type: EnterAddressSource? - MaxChainedCallsOnSameLine:FeeSelectorBlockContent.kt$state.selectedFeeItem.fee.amount.value.format { crypto( symbol = state.selectedFeeItem.fee.amount.currencySymbol, decimals = state.selectedFeeItem.fee.amount.decimals, ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) } - MultilineLambdaItParameter:DefaultSellRedirectDeepLinkHandler.kt$DefaultSellRedirectDeepLinkHandler${ Timber.e("Error on getting cryptoCurrency: $it") return@launch } - MultilineLambdaItParameter:FeeSelectorCustomFieldConverter.kt$FeeSelectorCustomFieldConverter${ when (it) { is Fee.Kaspa -> kaspaCustomFeeConverter.tryAutoFixValue( minimumFee = it, customValues = customValues, ) else -> customValues } } - MultilineLambdaItParameter:FeeSelectorModel.kt$FeeSelectorModel${ feeSelectorAlertFactory.getFeeUpdatedAlert( newFee = it, feeSelectorUM = uiState.value, proceedAction = { modelScope.launch { feeSelectorCheckReloadTrigger.callbackCheckResult(true) } }, stopAction = { modelScope.launch { feeSelectorCheckReloadTrigger.callbackCheckResult(false) } }, ) } - MultilineLambdaItParameter:KaspaCustomFeeConverter.kt$KaspaCustomFeeConverter${ val valueDecimal = it.value.parseToBigDecimal(it.decimals) // krc-20 transaction will be failed if custom fee value is less than minimum, // so we set value to minimum in this case if (valueDecimal < minimumFee.amount.value) { val fixedValue = minimumFeeAmountValue.parseBigDecimal(it.decimals) set( FEE_AMOUNT_INDEX, it.copy( value = fixedValue, label = getFiatReference( rate = currencyStatus.fiatRate, value = valueDecimal, appCurrency = appCurrency, ), ), ) } } - MultilineLambdaItParameter:NFTSendConfirmModel.kt$NFTSendConfirmModel${ it.copy( confirmUM = NFTSendConfirmInitialStateTransformer( isShowTapHelp = isShowTapHelp, walletName = stringReference(userWallet.name), ).transform(uiState.value.confirmUM), ) } - MultilineLambdaItParameter:NFTSendConfirmModel.kt$NFTSendConfirmModel${ it.copy( confirmUM = NFTSendConfirmationNotificationsTransformerV2( feeSelectorUM = uiState.value.feeSelectorUM, analyticsEventHandler = analyticsEventHandler, cryptoCurrency = cryptoCurrencyStatus.currency, appCurrency = params.appCurrency, analyticsCategoryName = analyticsCategoryName, ).transform(uiState.value.confirmUM), ) } - MultilineLambdaItParameter:NFTSendConfirmModel.kt$NFTSendConfirmModel${ val confirmUM = it.confirmUM as? ConfirmUM.Content it.copy(confirmUM = confirmUM?.copy(showTapHelp = showTapHelp) ?: it.confirmUM) } - MultilineLambdaItParameter:NFTSendConfirmModel.kt$NFTSendConfirmModel${ val isFeeNotNull = it.feeSelectorUM is FeeSelectorUMRedesigned.Content it.copy( confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( isPrimaryButtonEnabled = !hasError && isFeeNotNull, ) ?: it.confirmUM, ) } - MultilineLambdaItParameter:RecentListUtils.kt${ add( DestinationRecipientListUM( id = "$tag$it", isLoading = false, isVisible = false, ), ) } - MultilineLambdaItParameter:RecentListUtils.kt${ add( DestinationRecipientListUM( id = "$tag$it", isLoading = true, ), ) } - MultilineLambdaItParameter:SendAmountModel.kt$SendAmountModel${ (it as? AmountState.Data)?.copy( isPrimaryButtonEnabled = false, ) ?: it } - MultilineLambdaItParameter:SendAmountModel.kt$SendAmountModel${ EnterAmountBoundary( amount = it, fiatRate = cryptoCurrencyStatus.value.fiatRate.orZero(), ) } - MultilineLambdaItParameter:SendConfirmModel.kt$SendConfirmModel${ it.copy( confirmUM = SendConfirmInitialStateTransformer( isShowTapHelp = isShowTapHelp, walletName = stringReference(userWallet.name), ).transform(uiState.value.confirmUM), confirmData = confirmData, ) } - MultilineLambdaItParameter:SendConfirmModel.kt$SendConfirmModel${ it.copy( confirmUM = SendConfirmationNotificationsTransformerV2( feeSelectorUM = uiState.value.feeSelectorUM, amountUM = uiState.value.amountUM, analyticsEventHandler = analyticsEventHandler, cryptoCurrency = cryptoCurrencyStatus.currency, appCurrency = appCurrency, analyticsCategoryName = params.analyticsCategoryName, ).transform(uiState.value.confirmUM), ) } - MultilineLambdaItParameter:SendConfirmModel.kt$SendConfirmModel${ val confirmUM = it.confirmUM as? ConfirmUM.Content it.copy(confirmUM = confirmUM?.copy(showTapHelp = showTapHelp) ?: it.confirmUM) } - MultilineLambdaItParameter:SendConfirmModel.kt$SendConfirmModel${ val feeUM = it.feeSelectorUM as? FeeSelectorUMRedesigned.Content it.copy( confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( isPrimaryButtonEnabled = !hasError && feeUM != null, ) ?: it.confirmUM, ) } - MultilineLambdaItParameter:SendDestinationContent.kt${ if (it) { content() } else { Box(modifier = Modifier.fillMaxWidth()) } } - MultilineLambdaItParameter:SendDestinationModel.kt$SendDestinationModel${ analyticsEventHandler.send( SendDestinationAnalyticEvents.AddressEntered( categoryName = analyticsCategoryName, source = params.analyticsSendSource, method = it, isValid = addressValidationResult.isRight(), ), ) } - MultilineLambdaItParameter:SendDestinationModel.kt$SendDestinationModel${ if (it.network.rawId == cryptoCurrencyNetwork.rawId) { getNetworkAddressesUseCase.invokeSync( userWalletId = wallet.walletId, networkRawId = it.network.id.rawId, ) } else { null } } - MultilineLambdaItParameter:SendDestinationValidationResultTransformer.kt$SendDestinationValidationResultTransformer${ when (it) { is AddressValidation.Error.DataError, AddressValidation.Error.InvalidAddress, -> R.string.send_recipient_address_error AddressValidation.Error.AddressInWallet -> R.string.send_error_address_same_as_wallet } } - MultilineLambdaItParameter:SendModel.kt$SendModel${ Timber.w(it.toString()) showAlertError() return@launch } - MultilineLambdaItParameter:SendModel.kt$SendModel${ it.copy( destinationUM = SendDestinationInitialStateTransformer( cryptoCurrency = cryptoCurrency, ).transform(DestinationUM.Empty()), feeSelectorUM = FeeSelectorUM.Loading, confirmUM = ConfirmUM.Empty, confirmData = null, navigationUM = NavigationUM.Empty, ) } - MultilineLambdaItParameter:SendRecipientWalletListConverter.kt$SendRecipientWalletListConverter${ val isCoin = it.cryptoCurrency is CryptoCurrency.Coin val isNotSameAddress = it.address != senderAddress val isNotBlankAddress = it.address.isNotBlank() isNotBlankAddress && isCoin && (isNotSameAddress || isSelfSendAvailable) } - NamedArguments:EthereumCustomFeeConverter.kt$EthereumCustomFeeConverter$onValueChange(feeValue, customValues, index, value) - NamedArguments:FeeSelectorCustomValueChangedTransformer.kt$FeeSelectorCustomValueChangedTransformer$onValueChange(state, customFee.customValues, index, value) - NestedScopeFunctions:KaspaCustomFeeConverter.kt$KaspaCustomFeeConverter$let { val valueDecimal = it.value.parseToBigDecimal(it.decimals) // krc-20 transaction will be failed if custom fee value is less than minimum, // so we set value to minimum in this case if (valueDecimal < minimumFee.amount.value) { val fixedValue = minimumFeeAmountValue.parseBigDecimal(it.decimals) set( FEE_AMOUNT_INDEX, it.copy( value = fixedValue, label = getFiatReference( rate = currencyStatus.fiatRate, value = valueDecimal, appCurrency = appCurrency, ), ), ) } } - NoNameShadowing:FeeSelectorAlertFactory.kt$FeeSelectorAlertFactory$newFee - NoNameShadowing:SendContent.kt$navigationUM - NonBooleanPropertyPrefixedWithIs:FeeSelectorModel.kt$FeeSelectorModel$private val isFeeApproximateUseCase: IsFeeApproximateUseCase - NonBooleanPropertyPrefixedWithIs:NFTSendConfirmComponent.kt$NFTSendConfirmComponent.Params$val isBalanceHidingFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:NFTSendConfirmModel.kt$NFTSendConfirmModel$private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase - NonBooleanPropertyPrefixedWithIs:NFTSendModel.kt$NFTSendModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:NFTSendModel.kt$NFTSendModel$val isBalanceHiddenFlow: StateFlow<Boolean> field = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:NotificationsModel.kt$NotificationsModel$private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase - NonBooleanPropertyPrefixedWithIs:SendAmountComponentParams.kt$SendAmountComponentParams$abstract val isAccountModeFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendAmountComponentParams.kt$SendAmountComponentParams$abstract val isBalanceHidingFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendAmountComponentParams.kt$SendAmountComponentParams.AmountBlockParams$override val isAccountModeFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendAmountComponentParams.kt$SendAmountComponentParams.AmountBlockParams$override val isBalanceHidingFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendAmountComponentParams.kt$SendAmountComponentParams.AmountParams$override val isAccountModeFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendAmountComponentParams.kt$SendAmountComponentParams.AmountParams$override val isBalanceHidingFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendAmountModel.kt$SendAmountModel$val isSendWithSwapAvailable: StateFlow<Boolean> field = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:SendConfirmComponent.kt$SendConfirmComponent.Params$val isAccountModeFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendConfirmComponent.kt$SendConfirmComponent.Params$val isBalanceHidingFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendConfirmModel.kt$SendConfirmModel$private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase - NonBooleanPropertyPrefixedWithIs:SendConfirmModel.kt$SendConfirmModel$private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase - NonBooleanPropertyPrefixedWithIs:SendConfirmModel.kt$SendConfirmModel$val isBalanceHiddenFlow: StateFlow<Boolean> field = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:SendDestinationModel.kt$SendDestinationModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:SendDestinationModel.kt$SendDestinationModel$private val isSelfSendAvailableUseCase: IsSelfSendAvailableUseCase - NonBooleanPropertyPrefixedWithIs:SendModel.kt$SendModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:SendModel.kt$SendModel$val isAccountModeFlow: StateFlow<Boolean> field = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:SendModel.kt$SendModel$val isBalanceHiddenFlow: StateFlow<Boolean> field = MutableStateFlow(false) - NullCheckOnMutableProperty:SendAmountModel.kt$SendAmountModel$if (uiState.value is AmountState.Empty && userWallet != null) { val isOnlyOneWallet = getWalletsUseCase.invokeSync().size == 1 val walletTitle = if (isOnlyOneWallet) { resourceReference(R.string.send_from_title) } else { resourceReference( R.string.send_from_wallet_name, WrappedList(listOf(userWallet?.name.orEmpty())), // TODO AND-11440 ) } _uiState.update { AmountStateConverter( clickIntents = this, appCurrency = appCurrency, cryptoCurrencyStatus = cryptoCurrencyStatus, maxEnterAmount = maxAmountBoundary, iconStateConverter = CryptoCurrencyToIconStateConverter(), isBalanceHidden = params.isBalanceHidingFlow.value, accountTitleUM = AmountAccountConverter( isAccountsMode = isAccountsMode, walletTitle = walletTitle, prefixText = resourceReference(R.string.common_from), ).convert(account), ).convert( AmountParameters( title = walletTitle, value = "", ), ) } } - NullableToStringCall:BitcoinCustomFeeConverter.kt$BitcoinCustomFeeConverter$toSatoshiPerByte( amount = feeValue, decimals = value.amount.decimals, txSize = value.txSize, ).toString() - NullableToStringCall:NFTSendConfirmModel.kt$NFTSendConfirmModel$params.nftAsset.amount.toString() - ReusedModifierInstance:DefaultSendEntryPointComponent.kt$DefaultSendEntryPointComponent$Content(modifier.fillMaxSize()) - UnnecessaryLet:EthereumCustomFeeConverter.kt$EthereumCustomFeeConverter$let(::add) - UnnecessaryLet:EthereumCustomFeeConverter.kt$EthereumCustomFeeConverter$let(::addAll) - UnnecessaryLet:SendAmountModel.kt$SendAmountModel$let { onCurrencyChangeClick(isEnterInFiat) } - UseEmptyCounterpart:NFTSendAnalyticEvents.kt$NFTSendAnalyticEvents$mapOf() - UseEmptyCounterpart:NotificationsAnalyticEvents.kt$NotificationsAnalyticEvents$mapOf() - UseEmptyCounterpart:SendAnalyticEvents.kt$SendAnalyticEvents$mapOf() - UseEmptyCounterpart:SendDestinationAnalyticEvents.kt$SendDestinationAnalyticEvents$mapOf() - VarCouldBeVal:SendDestinationModel.kt$SendDestinationModel$private var validationJobHolder = JobHolder() - VarCouldBeVal:SendModel.kt$SendModel$private var balanceHidingJobHolder = JobHolder() - - diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt index cd6bfb3235..43924cbf0e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt @@ -85,14 +85,14 @@ internal fun SendContent( @Composable private fun SendAppBar(navigationUM: NavigationUM) { - val navigationUM = navigationUM as? NavigationUM.Content ?: return + val navigationUMContent = navigationUM as? NavigationUM.Content ?: return AppBarWithBackButtonAndIcon( - text = navigationUM.title.resolveReference(), - subtitle = navigationUM.subtitle?.resolveReference(), - onBackClick = navigationUM.backIconClick, - onIconClick = navigationUM.additionalIconClick, - backIconRes = navigationUM.backIconRes, - iconRes = navigationUM.additionalIconRes, + text = navigationUMContent.title.resolveReference(), + subtitle = navigationUMContent.subtitle?.resolveReference(), + onBackClick = navigationUMContent.backIconClick, + onIconClick = navigationUMContent.additionalIconClick, + backIconRes = navigationUMContent.backIconRes, + iconRes = navigationUMContent.additionalIconRes, backgroundColor = TangemTheme.colors.background.tertiary, modifier = Modifier.height(TangemTheme.dimens.size56), ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/TapHelp.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/TapHelp.kt index 480650ee0e..43251bec2f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/TapHelp.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/TapHelp.kt @@ -22,14 +22,14 @@ private const val TAP_HELP_ANIMATION_DELAY = 500L internal fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modifier) { item(key = TAP_HELP_KEY) { - var wrappedIsDisplay by remember { mutableStateOf(false) } + var isWrappedDisplay by remember { mutableStateOf(false) } LaunchedEffect(key1 = isDisplay) { delay(TAP_HELP_ANIMATION_DELAY) - wrappedIsDisplay = isDisplay + isWrappedDisplay = isDisplay } - if (wrappedIsDisplay) { + if (isWrappedDisplay) { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/ConfirmUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/ConfirmUM.kt index 78cc00c705..7a63af4176 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/ConfirmUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/ConfirmUM.kt @@ -14,7 +14,7 @@ internal sealed class ConfirmUM { override val isPrimaryButtonEnabled: Boolean = false, val walletName: TextReference, val isSending: Boolean, - val showTapHelp: Boolean, + val isShowTapHelp: Boolean, val sendingFooter: TextReference, val notifications: ImmutableList, ) : ConfirmUM() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt index 825518011b..402cd981c5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt @@ -51,8 +51,8 @@ internal class DefaultSellRedirectDeepLinkHandler @AssistedInject constructor( } scope.launch { - val cryptoCurrency = getCryptoCurrencyUseCase(userWallet, currencyId).getOrElse { - Timber.e("Error on getting cryptoCurrency: $it") + val cryptoCurrency = getCryptoCurrencyUseCase(userWallet, currencyId).getOrElse { error -> + Timber.e("Error on getting cryptoCurrency: $error") return@launch } // Convert using universal parser to account for regional separators diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt index b9f865983a..9e41df2a03 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt @@ -106,6 +106,7 @@ internal class DefaultSendEntryPointComponent @AssistedInject constructor( Children( stack = childStackValue, + modifier = modifier, animation = stackAnimation { child -> when (child.configuration) { SendEntryRoute.Send, @@ -115,7 +116,7 @@ internal class DefaultSendEntryPointComponent @AssistedInject constructor( } }, ) { child -> - child.instance.Content(modifier.fillMaxSize()) + child.instance.Content(Modifier.fillMaxSize()) } } @@ -125,7 +126,7 @@ internal class DefaultSendEntryPointComponent @AssistedInject constructor( ): ComposableContentComponent = when (configuration) { is SendEntryRoute.ChooseToken -> getManagedTokensComponent( componentContext = factoryContext, - showSendViaSwapNotification = configuration.showSendViaSwapNotification, + showSendViaSwapNotification = configuration.isShowSendViaSwapNotification, ) SendEntryRoute.Send -> sendComponent SendEntryRoute.SendWithSwap -> sendWithSwapComponent diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt index 02ba094907..f3ff4ee117 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt @@ -42,12 +42,12 @@ internal class SendEntryPointModel @Inject constructor( lastSavedAmount = lastAmount isEnterInFiat = isEnterInFiatSelected modelScope.launch { - val showSendViaSwapNotification = shouldShowNotificationUseCase( + val isShowSendViaSwapNotification = shouldShowNotificationUseCase( NotificationId.SendViaSwapTokenSelectorNotification.key, ) router.push( SendEntryRoute.ChooseToken( - showSendViaSwapNotification = showSendViaSwapNotification, + isShowSendViaSwapNotification = isShowSendViaSwapNotification, ), ) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorAlertFactory.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorAlertFactory.kt index 43091d08a1..6abebb0fb4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorAlertFactory.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorAlertFactory.kt @@ -20,10 +20,10 @@ internal class FeeSelectorAlertFactory @Inject constructor( ) { fun checkAndShowAlerts(feeSelectorUM: FeeSelectorUM.Content, onConfirmClick: () -> Unit) { - val showFeeTooLow = checkAndShowFeeTooLow(feeSelectorUM, onConfirmClick) - val showFeeTooHigh = checkAndShowFeeTooHigh(feeSelectorUM, onConfirmClick) + val isShowFeeTooLow = checkAndShowFeeTooLow(feeSelectorUM, onConfirmClick) + val isShowFeeTooHigh = checkAndShowFeeTooHigh(feeSelectorUM, onConfirmClick) - if (!showFeeTooLow && !showFeeTooHigh) { + if (!isShowFeeTooLow && !isShowFeeTooHigh) { onConfirmClick() } } @@ -78,20 +78,20 @@ internal class FeeSelectorAlertFactory @Inject constructor( } fun getFeeUpdatedAlert( - newFee: TransactionFee, + newTransactionFee: TransactionFee, feeSelectorUM: FeeSelectorUM, proceedAction: () -> Unit, stopAction: () -> Unit, ) { if (feeSelectorUM !is FeeSelectorUM.Content) return - val newFee = when (newFee) { - is TransactionFee.Single -> newFee.normal + val newFee = when (newTransactionFee) { + is TransactionFee.Single -> newTransactionFee.normal is TransactionFee.Choosable -> { when (feeSelectorUM.selectedFeeItem) { is FeeItem.Suggested -> feeSelectorUM.selectedFeeItem.fee - is FeeItem.Slow -> newFee.minimum - is FeeItem.Market -> newFee.normal - is FeeItem.Fast -> newFee.priority + is FeeItem.Slow -> newTransactionFee.minimum + is FeeItem.Market -> newTransactionFee.normal + is FeeItem.Fast -> newTransactionFee.priority is FeeItem.Custom -> return } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt index ba91d7d75e..c290cc9fb3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt @@ -197,7 +197,7 @@ internal class FeeSelectorModel @Inject constructor( private fun subscribeOnFeeReloadTriggerUpdates() { feeSelectorReloadListener.reloadTriggerFlow .onEach { data -> - if (data.removeSuggestedFee) { + if (data.isRemoveSuggestedFee) { uiState.update(FeeSelectorRemoveSuggestedTransformer) } loadFee() @@ -220,9 +220,9 @@ internal class FeeSelectorModel @Inject constructor( private fun checkLoadFee() { modelScope.launch { params.onLoadFee().fold( - ifRight = { + ifRight = { newFee -> feeSelectorAlertFactory.getFeeUpdatedAlert( - newFee = it, + newTransactionFee = newFee, feeSelectorUM = uiState.value, proceedAction = { modelScope.launch { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt index 258312e8a0..344bb80387 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt @@ -107,10 +107,10 @@ internal class FeeSelectorCustomFieldConverter( when (val fees = feeSelectorState.fees) { is TransactionFee.Choosable -> fees.minimum is TransactionFee.Single -> fees.normal - }.let { - when (it) { + }.let { fee -> + when (fee) { is Fee.Kaspa -> kaspaCustomFeeConverter.tryAutoFixValue( - minimumFee = it, + minimumFee = fee, customValues = customValues, ) else -> customValues diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomValueChangedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomValueChangedTransformer.kt index 9418304d3b..5e94e5a7c1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomValueChangedTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomValueChangedTransformer.kt @@ -27,7 +27,12 @@ internal class FeeSelectorCustomValueChangedTransformer( feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, normalFee = state.selectedFeeItem.fee, ) - val updatedCustomValues = customFeeConverter.onValueChange(state, customFee.customValues, index, value) + val updatedCustomValues = customFeeConverter.onValueChange( + feeSelectorState = state, + customValues = customFee.customValues, + index = index, + value = value, + ) val newCustomFee = customFee.copy( fee = customFeeConverter.convertBack(updatedCustomValues), customValues = updatedCustomValues, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt index 30dcb3c687..5eb28eda97 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt @@ -172,12 +172,13 @@ private fun FeeContent(state: FeeSelectorUM.Content, modifier: Modifier = Modifi approximate = state.feeExtraInfo.isFeeApproximate, ) } else { - state.selectedFeeItem.fee.amount.value.format { - crypto( - symbol = state.selectedFeeItem.fee.amount.currencySymbol, - decimals = state.selectedFeeItem.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - } + state.selectedFeeItem.fee.amount.value + .format { + crypto( + symbol = state.selectedFeeItem.fee.amount.currencySymbol, + decimals = state.selectedFeeItem.fee.amount.decimals, + ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) + } }, style = TangemTheme.typography.body1, color = TangemTheme.colors.text.tertiary, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index 0027bda872..daa164aa95 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -129,7 +129,7 @@ private fun FeeSelectorItems( val feeFiatRateUM = state.feeFiatRateUM state.feeItems.fastForEachIndexed { index, item -> val isSelected = item.isSameClass(state.selectedFeeItem) - val lastItem = index == state.feeItems.size - 1 + val isLastItem = index == state.feeItems.size - 1 val iconTint by animateColorAsState( targetValue = if (isSelected) TangemTheme.colors.icon.accent else TangemTheme.colors.text.tertiary, label = "Fee selector icon tint change", @@ -180,7 +180,7 @@ private fun FeeSelectorItems( null }, ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !lastItem, + showDivider = !isSelected && !isLastItem, ) } } @@ -243,7 +243,7 @@ private fun ExpandedCustomFeeItems( ) { Column(modifier = modifier) { customFeeFields.fastForEachIndexed { index, field -> - val showDivider = index != customFeeFields.size - 1 || nonce is FeeNonce.Nonce + val isShowDivider = index != customFeeFields.size - 1 || nonce is FeeNonce.Nonce if (field.label != null) { InputRowEnterInfoAmountV2( text = field.value, @@ -256,7 +256,7 @@ private fun ExpandedCustomFeeItems( keyboardOptions = field.keyboardOptions, keyboardActions = field.keyboardActions, onValueChange = { onValueChange(index, it) }, - showDivider = showDivider, + showDivider = isShowDivider, isReadOnly = field.isReadonly, ) } else { @@ -270,7 +270,7 @@ private fun ExpandedCustomFeeItems( onValueChange = { onValueChange(index, it) }, keyboardOptions = field.keyboardOptions, keyboardActions = field.keyboardActions, - showDivider = showDivider, + showDivider = isShowDivider, ) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 858747f091..d6e89cd52c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -88,10 +88,17 @@ internal class DefaultSendComponent @AssistedInject constructor( componentScope.launch { when (val activeComponent = stack.active.instance) { is SendConfirmComponent -> { + val fromCurrency = params.currency + val fromDerivationIndex = model.accountFlow.value?.derivationIndex?.value + .takeIf { model.isAccountModeFlow.value } analyticsEventHandler.send( CommonSendAnalyticEvents.ConfirmationScreenOpened( categoryName = model.analyticCategoryName, source = model.analyticsSendSource, + sendBlockchain = fromCurrency.network.name, + sendToken = fromCurrency.symbol, + fromDerivationIndex = fromDerivationIndex, + toDerivationIndex = null, ), ) if (model.currentRoute.value.isEditMode) { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt index 4c2f12ab7a..d8ccef5d07 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt @@ -2,6 +2,7 @@ package com.tangem.features.send.v2.send.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_FROM import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.ENS_ADDRESS import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE @@ -15,7 +16,7 @@ import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents */ internal sealed class SendAnalyticEvents( event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = CommonSendAnalyticEvents.SEND_CATEGORY, event = event, params = params) { /** Transaction send screen opened */ @@ -23,20 +24,23 @@ internal sealed class SendAnalyticEvents( val token: String, val feeType: AnalyticsParam.FeeType, val blockchain: String, - val nonceNotEmpty: Boolean, - private val ensStatus: AnalyticsParam.EnsStatus, + val isNonceNotEmpty: Boolean, + private val ensStatus: AnalyticsParam.EmptyFull, + val derivationIndex: Int?, ) : SendAnalyticEvents( event = "Transaction Sent Screen Opened", - params = mapOf( - TOKEN_PARAM to token, - FEE_TYPE to feeType.value, - BLOCKCHAIN to blockchain, - NONCE to nonceNotEmpty.toString().capitalize(), - ENS_ADDRESS to when (ensStatus) { - AnalyticsParam.EnsStatus.EMPTY -> false.toString().capitalize() - AnalyticsParam.EnsStatus.FULL -> true.toString().capitalize() - }, - ), + params = buildMap { + put(TOKEN_PARAM, token) + put(FEE_TYPE, feeType.value) + put(BLOCKCHAIN, blockchain) + if (derivationIndex != null) put(ACCOUNT_DERIVATION_FROM, derivationIndex.toString()) + put(NONCE, isNonceNotEmpty.toString().capitalize()) + val ensAddress = when (ensStatus) { + AnalyticsParam.EmptyFull.Empty -> false.toString().capitalize() + AnalyticsParam.EmptyFull.Full -> true.toString().capitalize() + } + put(ENS_ADDRESS, ensAddress) + }, ) data class ConvertTokenButtonClicked( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt index 0d5fe7f1e4..e65f75d699 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt @@ -4,6 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped +import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.send.v2.api.entity.FeeNonce import com.tangem.features.send.v2.api.entity.FeeSelectorUM @@ -17,17 +18,20 @@ internal class SendAnalyticHelper @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, ) { - fun sendSuccessAnalytics(cryptoCurrency: CryptoCurrency, sendUM: SendUM) { + fun sendSuccessAnalytics(cryptoCurrency: CryptoCurrency, sendUM: SendUM, account: Account.CryptoPortfolio?) { val destinationUM = sendUM.destinationUM as? DestinationUM.Content val feeSelectorUM = sendUM.feeSelectorUM as? FeeSelectorUM.Content ?: return val feeType = feeSelectorUM.toAnalyticType() + val isNotMainAccount = account != null && !account.isMainAccount + val derivationIndex = if (isNotMainAccount) account.derivationIndex.value else null analyticsEventHandler.send( SendAnalyticEvents.TransactionScreenOpened( token = cryptoCurrency.symbol, feeType = feeType, blockchain = cryptoCurrency.network.name, - nonceNotEmpty = feeSelectorUM.feeNonce is FeeNonce.Nonce, + isNonceNotEmpty = feeSelectorUM.feeNonce is FeeNonce.Nonce, ensStatus = getEnsStatus(sendUM), + derivationIndex = derivationIndex, ), ) analyticsEventHandler.send( @@ -53,13 +57,13 @@ internal class SendAnalyticHelper @Inject constructor( } } - private fun getEnsStatus(sendUM: SendUM): AnalyticsParam.EnsStatus { - val blockchainAddressForEns = + private fun getEnsStatus(sendUM: SendUM): AnalyticsParam.EmptyFull { + val isBlockchainAddressForEns = (sendUM.destinationUM as? DestinationUM.Content)?.addressTextField?.isAddressEns - return if (blockchainAddressForEns == true) { - AnalyticsParam.EnsStatus.FULL + return if (isBlockchainAddressForEns == true) { + AnalyticsParam.EmptyFull.Full } else { - AnalyticsParam.EnsStatus.EMPTY + AnalyticsParam.EmptyFull.Empty } } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index 5c00ee1e19..6fcd04be67 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -306,8 +306,8 @@ internal class SendConfirmModel @Inject constructor( modelScope.launch { val isShowTapHelp = isSendTapHelpEnabledUseCase.invokeSync().getOrElse { false } if (confirmUM is ConfirmUM.Empty) { - _uiState.update { - it.copy( + _uiState.update { state -> + state.copy( confirmUM = SendConfirmInitialStateTransformer( isShowTapHelp = isShowTapHelp, walletName = stringReference(userWallet.name), @@ -323,9 +323,9 @@ internal class SendConfirmModel @Inject constructor( private fun subscribeOnTapHelpUpdates() { isSendTapHelpEnabledUseCase().getOrNull() ?.onEach { showTapHelp -> - _uiState.update { - val confirmUM = it.confirmUM as? ConfirmUM.Content - it.copy(confirmUM = confirmUM?.copy(showTapHelp = showTapHelp) ?: it.confirmUM) + _uiState.update { state -> + val confirmUM = state.confirmUM as? ConfirmUM.Content + state.copy(confirmUM = confirmUM?.copy(isShowTapHelp = showTapHelp) ?: state.confirmUM) } }?.launchIn(modelScope) } @@ -333,12 +333,12 @@ internal class SendConfirmModel @Inject constructor( private fun subscribeOnNotificationsUpdateTrigger() { notificationsUpdateListener.hasErrorFlow .onEach { hasError -> - _uiState.update { - val feeUM = it.feeSelectorUM as? FeeSelectorUMRedesigned.Content - it.copy( - confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( + _uiState.update { state -> + val feeUM = state.feeSelectorUM as? FeeSelectorUMRedesigned.Content + state.copy( + confirmUM = (state.confirmUM as? ConfirmUM.Content)?.copy( isPrimaryButtonEnabled = !hasError && feeUM != null, - ) ?: it.confirmUM, + ) ?: state.confirmUM, ) } } @@ -416,7 +416,11 @@ internal class SendConfirmModel @Inject constructor( updateTransactionStatus(txData) addTokenToWalletIfNeeded() sendBalanceUpdater.scheduleUpdates() - sendAnalyticHelper.sendSuccessAnalytics(cryptoCurrency, uiState.value) + sendAnalyticHelper.sendSuccessAnalytics( + cryptoCurrency = cryptoCurrency, + sendUM = uiState.value, + account = params.accountFlow.value, + ) params.callback.onResult(uiState.value) params.onSendTransaction() }, @@ -495,8 +499,8 @@ internal class SendConfirmModel @Inject constructor( feeError = confirmData.feeError, ), ) - _uiState.update { - it.copy( + _uiState.update { state -> + state.copy( confirmUM = SendConfirmationNotificationsTransformerV2( feeSelectorUM = uiState.value.feeSelectorUM, amountUM = uiState.value.amountUM, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt index 1231a4954e..bd50b38485 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt @@ -13,7 +13,7 @@ internal class SendConfirmInitialStateTransformer( return ConfirmUM.Content( walletName = walletName, isSending = false, - showTapHelp = isShowTapHelp, + isShowTapHelp = isShowTapHelp, sendingFooter = TextReference.EMPTY, notifications = persistentListOf(), ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt index d617e88182..cb97fc4632 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt @@ -52,7 +52,7 @@ internal fun SendConfirmContent( feeSelectorBlockComponent = feeSelectorBlockComponent, ) if (confirmUM != null) { - tapHelp(isDisplay = confirmUM.showTapHelp) + tapHelp(isDisplay = confirmUM.isShowTapHelp) with(notificationsComponent) { content( state = notificationsUM, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index 38c5c33abd..1028a207b5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -147,7 +147,7 @@ internal class SendModel @Inject constructor( var appCurrency: AppCurrency = AppCurrency.Default var predefinedValues: PredefinedValues = PredefinedValues.Empty - private var balanceHidingJobHolder = JobHolder() + private val balanceHidingJobHolder = JobHolder() init { subscribeOnBalanceHidden() @@ -226,8 +226,8 @@ internal class SendModel @Inject constructor( } override fun resetSendNavigation() { - uiState.update { - it.copy( + uiState.update { state -> + state.copy( destinationUM = SendDestinationInitialStateTransformer( cryptoCurrency = cryptoCurrency, ).transform(DestinationUM.Empty()), @@ -358,8 +358,8 @@ internal class SendModel @Inject constructor( ) } }, - ifLeft = { - Timber.w(it.toString()) + ifLeft = { error -> + Timber.w(error.toString()) showAlertError() return@launch }, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt index fbd5a692d6..7d07f291bd 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt @@ -29,17 +29,17 @@ import kotlinx.coroutines.delay @Composable internal fun SendConfirmSuccessContent(sendUM: SendUM, destinationBlockComponent: SendDestinationBlockComponent) { - var visible by remember { mutableStateOf(false) } + var isVisible by remember { mutableStateOf(false) } LaunchedEffect(Unit) { delay(ANIMATION_DELAY) - visible = true + isVisible = true } val height = ANIMATION_OFFSET.toPx().toInt() AnimatedVisibility( - visible = visible, + visible = isVisible, enter = slideInVertically( initialOffsetY = { height }, ).plus(fadeIn()), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt index c8992e8ca4..250a2adb28 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt @@ -79,10 +79,17 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( componentScope.launch { when (val activeComponent = stack.active.instance) { is NFTSendConfirmComponent -> { + val fromCurrency = model.cryptoCurrency + val fromDerivationIndex = model.account?.derivationIndex?.value + .takeIf { model.isAccountsMode } analyticsEventHandler.send( CommonSendAnalyticEvents.ConfirmationScreenOpened( categoryName = analyticsCategoryName, source = analyticsSendSource, + sendBlockchain = fromCurrency.network.name, + sendToken = fromCurrency.symbol, + fromDerivationIndex = fromDerivationIndex, + toDerivationIndex = null, ), ) if (model.currentRouteFlow.value.isEditMode) { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt index f542b5f217..25700edaa6 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt @@ -14,7 +14,7 @@ import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents */ internal sealed class NFTSendAnalyticEvents( event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = CommonSendAnalyticEvents.NFT_SEND_CATEGORY, event = event, params = params) { /** Transaction send screen opened */ @@ -22,14 +22,14 @@ internal sealed class NFTSendAnalyticEvents( val token: String, val feeType: AnalyticsParam.FeeType, val blockchain: String, - val nonceNotEmpty: Boolean, + val isNonceNotEmpty: Boolean, ) : NFTSendAnalyticEvents( event = "NFT Sent Screen Opened", params = mapOf( TOKEN_PARAM to token, FEE_TYPE to feeType.value, BLOCKCHAIN to blockchain, - NONCE to nonceNotEmpty.toString().capitalize(), + NONCE to isNonceNotEmpty.toString().capitalize(), ), ) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt index c25e1fc99a..bab316c20f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt @@ -27,7 +27,7 @@ internal class NFTSendAnalyticHelper @Inject constructor( token = cryptoCurrency.symbol, feeType = feeType, blockchain = cryptoCurrency.network.name, - nonceNotEmpty = feeSelectorUM.feeNonce is FeeNonce.Nonce, + isNonceNotEmpty = feeSelectorUM.feeNonce is FeeNonce.Nonce, ), ) analyticsEventHandler.send( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index 1c7c92bc7e..8bab99a2a4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -195,7 +195,7 @@ internal class NFTSendConfirmModel @Inject constructor( derivationPath = cryptoCurrency.network.derivationPath.value, destinationAddress = confirmData.enteredDestination.orEmpty(), tokenSymbol = null, - amount = params.nftAsset.amount.toString(), + amount = params.nftAsset.amount?.toString().orEmpty(), fee = confirmData.fee?.amount?.value?.stripZeroPlainString(), ), ) @@ -213,8 +213,8 @@ internal class NFTSendConfirmModel @Inject constructor( modelScope.launch { val isShowTapHelp = isSendTapHelpEnabledUseCase.invokeSync().getOrElse { false } if (confirmUM is ConfirmUM.Empty || isEmptyFee) { - _uiState.update { - it.copy( + _uiState.update { state -> + state.copy( confirmUM = NFTSendConfirmInitialStateTransformer( isShowTapHelp = isShowTapHelp, walletName = stringReference(userWallet.name), @@ -229,9 +229,9 @@ internal class NFTSendConfirmModel @Inject constructor( private fun subscribeOnTapHelpUpdates() { isSendTapHelpEnabledUseCase().getOrNull() ?.onEach { showTapHelp -> - _uiState.update { - val confirmUM = it.confirmUM as? ConfirmUM.Content - it.copy(confirmUM = confirmUM?.copy(showTapHelp = showTapHelp) ?: it.confirmUM) + _uiState.update { state -> + val confirmUM = state.confirmUM as? ConfirmUM.Content + state.copy(confirmUM = confirmUM?.copy(isShowTapHelp = showTapHelp) ?: state.confirmUM) } }?.launchIn(modelScope) } @@ -239,13 +239,13 @@ internal class NFTSendConfirmModel @Inject constructor( private fun subscribeOnNotificationsUpdateTrigger() { notificationsUpdateListener.hasErrorFlow .onEach { hasError -> - _uiState.update { - val isFeeNotNull = it.feeSelectorUM is FeeSelectorUMRedesigned.Content + _uiState.update { state -> + val isFeeNotNull = state.feeSelectorUM is FeeSelectorUMRedesigned.Content - it.copy( - confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( + state.copy( + confirmUM = (state.confirmUM as? ConfirmUM.Content)?.copy( isPrimaryButtonEnabled = !hasError && isFeeNotNull, - ) ?: it.confirmUM, + ) ?: state.confirmUM, ) } } @@ -353,8 +353,8 @@ internal class NFTSendConfirmModel @Inject constructor( ), ) } - _uiState.update { - it.copy( + _uiState.update { state -> + state.copy( confirmUM = NFTSendConfirmationNotificationsTransformerV2( feeSelectorUM = uiState.value.feeSelectorUM, analyticsEventHandler = analyticsEventHandler, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt index e7f383dbe3..de24a6a022 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt @@ -12,7 +12,7 @@ internal class NFTSendConfirmInitialStateTransformer( override fun transform(prevState: ConfirmUM): ConfirmUM { return ConfirmUM.Content( isSending = false, - showTapHelp = isShowTapHelp, + isShowTapHelp = isShowTapHelp, sendingFooter = TextReference.EMPTY, walletName = walletName, notifications = persistentListOf(), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt index 3f93769f0e..c950a991ae 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt @@ -45,7 +45,7 @@ internal fun NFTSendConfirmContent( feeSelectorBlockComponent = feeSelectorBlockComponent, ) if (confirmUM != null) { - tapHelp(isDisplay = confirmUM.showTapHelp) + tapHelp(isDisplay = confirmUM.isShowTapHelp) with(notificationsComponent) { content( state = notificationsUM, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt index c90b4429e3..b5e13f0e06 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt @@ -34,17 +34,17 @@ internal fun NFTSendSuccessContent( nftDetailsBlockComponent: NFTDetailsBlockComponent, modifier: Modifier = Modifier, ) { - var visible by remember { mutableStateOf(false) } + var isVisible by remember { mutableStateOf(false) } LaunchedEffect(Unit) { delay(ANIMATION_DELAY) - visible = true + isVisible = true } val height = ANIMATION_OFFSET.toPx().toInt() AnimatedVisibility( - visible = visible, + visible = isVisible, enter = slideInVertically( initialOffsetY = { height }, ).plus(fadeIn()), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt index 9be514d748..b232d081f9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -155,9 +155,9 @@ internal class SendAmountModel @Inject constructor( minAmountBoundary = getMinimumTransactionAmountSyncUseCase( userWalletId = params.userWalletId, cryptoCurrencyStatus = cryptoCurrencyStatus, - ).getOrNull()?.let { + ).getOrNull()?.let { amount -> EnterAmountBoundary( - amount = it, + amount = amount, fiatRate = cryptoCurrencyStatus.value.fiatRate.orZero(), ) } @@ -183,6 +183,7 @@ internal class SendAmountModel @Inject constructor( account: Account.CryptoPortfolio?, isAccountsMode: Boolean, ) { + val userWallet = userWallet if (uiState.value is AmountState.Empty && userWallet != null) { val isOnlyOneWallet = getWalletsUseCase.invokeSync().size == 1 val walletTitle = if (isOnlyOneWallet) { @@ -190,7 +191,7 @@ internal class SendAmountModel @Inject constructor( } else { resourceReference( R.string.send_from_wallet_name, - WrappedList(listOf(userWallet?.name.orEmpty())), // TODO [REDACTED_TASK_KEY] + WrappedList(listOf(userWallet.name)), // TODO [REDACTED_TASK_KEY] ) } _uiState.update { @@ -297,10 +298,10 @@ internal class SendAmountModel @Inject constructor( private fun confirmConvertToToken() { val amountParams = params as? SendAmountComponentParams.AmountParams ?: return val amountFieldData = uiState.value as? AmountState.Data - _uiState.update { - (it as? AmountState.Data)?.copy( + _uiState.update { state -> + (state as? AmountState.Data)?.copy( isPrimaryButtonEnabled = false, - ) ?: it + ) ?: state } val isEnterInFiatSelected = amountFieldData?.amountTextField?.isFiatValue == true @@ -352,7 +353,9 @@ internal class SendAmountModel @Inject constructor( private fun subscribeOnAmountUpdateTriggerUpdates() { sendAmountUpdateListener.updateAmountTriggerFlow .onEach { (amount, isEnterInFiat) -> - isEnterInFiat?.let { onCurrencyChangeClick(isEnterInFiat) } + if (isEnterInFiat != null) { + onCurrencyChangeClick(isEnterInFiat) + } onAmountValueChange(amount) saveResult() }.launchIn(modelScope) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/SendDestinationAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/SendDestinationAnalyticEvents.kt index 39191837d0..2907e373d7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/SendDestinationAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/SendDestinationAnalyticEvents.kt @@ -8,7 +8,7 @@ import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents internal sealed class SendDestinationAnalyticEvents( category: String, event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = category, event = event, params = params) { abstract val categoryName: String diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 5870afd0fb..ba2ab8f1b1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -85,7 +85,7 @@ internal class SendDestinationModel @Inject constructor( private val senderAddresses = MutableStateFlow>(emptyList()) - private var validationJobHolder = JobHolder() + private val validationJobHolder = JobHolder() init { configDestinationNavigation() @@ -234,11 +234,11 @@ internal class SendDestinationModel @Inject constructor( .map { wallet -> async { val addresses = if (!wallet.isMultiCurrency) { - getCryptoCurrencyUseCase(wallet.walletId).getOrNull()?.let { - if (it.network.rawId == cryptoCurrencyNetwork.rawId) { + getCryptoCurrencyUseCase(wallet.walletId).getOrNull()?.let { cryptoCurrency -> + if (cryptoCurrency.network.rawId == cryptoCurrencyNetwork.rawId) { getNetworkAddressesUseCase.invokeSync( userWalletId = wallet.walletId, - networkRawId = it.network.id.rawId, + networkRawId = cryptoCurrency.network.id.rawId, ) } else { null @@ -323,12 +323,12 @@ internal class SendDestinationModel @Inject constructor( network = cryptoCurrency.network, ) - type?.let { + if (type != null) { analyticsEventHandler.send( SendDestinationAnalyticEvents.AddressEntered( categoryName = analyticsCategoryName, source = params.analyticsSendSource, - method = it, + method = type, isValid = addressValidationResult.isRight(), ), ) @@ -339,12 +339,14 @@ internal class SendDestinationModel @Inject constructor( memoValidationResult, ), ) - autoNextFromRecipient(type, addressValidationResult.isRight(), memoValidationResult.isRight()) + if (type != null) { + autoNextFromRecipient(type, addressValidationResult.isRight(), memoValidationResult.isRight()) + } }.saveIn(validationJobHolder) } - private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean, isValidMemo: Boolean) { - if (type?.isAutoNext == true && isValidAddress && isValidMemo) { + private fun autoNextFromRecipient(type: EnterAddressSource, isValidAddress: Boolean, isValidMemo: Boolean) { + if (type.isAutoNext && isValidAddress && isValidMemo) { saveResult() (params as? SendDestinationComponentParams.DestinationParams)?.callback?.onNextClick() } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientHistoryListConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientHistoryListConverter.kt index 1b9d4bcbc9..6b9979cbfa 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientHistoryListConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientHistoryListConverter.kt @@ -39,8 +39,8 @@ internal class SendRecipientHistoryListConverter( } else { item.sourceType is TxInfo.SourceType.Single } - val notZero = !item.amount.isZero() - isTransfer && isSingleAddress && isNotContract && item.isOutgoing && notZero + val isNotZero = !item.amount.isZero() + isTransfer && isSingleAddress && isNotContract && item.isOutgoing && isNotZero } .take(RECENT_LIST_SIZE) .mapIndexed { index, tx -> diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt index 1e43755cbc..94223cd153 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt @@ -30,10 +30,10 @@ internal class SendRecipientWalletListConverter( var walletsCounter = 0 return this.filterNotNull() - .filter { - val isCoin = it.cryptoCurrency is CryptoCurrency.Coin - val isNotSameAddress = it.address != senderAddress - val isNotBlankAddress = it.address.isNotBlank() + .filter { destinationWallet -> + val isCoin = destinationWallet.cryptoCurrency is CryptoCurrency.Coin + val isNotSameAddress = destinationWallet.address != senderAddress + val isNotBlankAddress = destinationWallet.address.isNotBlank() isNotBlankAddress && isCoin && (isNotSameAddress || isSelfSendAvailable) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/RecentListUtils.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/RecentListUtils.kt index ad2e643f1e..638d7abede 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/RecentListUtils.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/RecentListUtils.kt @@ -9,10 +9,10 @@ internal const val WALLET_KEY_TAG = "wallet" internal const val RECENT_KEY_TAG = "recent" internal fun loadingListState(tag: String, count: Int) = buildList { - repeat(count) { + repeat(count) { i -> add( DestinationRecipientListUM( - id = "$tag$it", + id = "$tag$i", isLoading = true, ), ) @@ -20,10 +20,10 @@ internal fun loadingListState(tag: String, count: Int) = buildList { }.toPersistentList() internal fun emptyListState(tag: String, count: Int) = buildList { - repeat(count) { + repeat(count) { i -> add( DestinationRecipientListUM( - id = "$tag$it", + id = "$tag$i", isLoading = false, isVisible = false, ), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt index da3d010f3f..35cae2c471 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt @@ -21,8 +21,8 @@ internal class SendDestinationValidationResultTransformer( val isValidAddress = addressValidationResult.isRight() val isValidMemo = memoValidationResult.isRight() - val addressErrorText = addressValidationResult.mapLeft { - when (it) { + val addressErrorText = addressValidationResult.mapLeft { error -> + when (error) { is AddressValidation.Error.DataError, AddressValidation.Error.InvalidAddress, -> R.string.send_recipient_address_error diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt index 96592fdf28..4c18c81642 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt @@ -67,10 +67,13 @@ internal fun SendDestinationContent( onAddressChange = clickIntents::onRecipientAddressValueChange, onQrCodeClick = clickIntents::onQrCodeScanClick, ) - memoField( - memoField = state.memoTextField, - onMemoChange = clickIntents::onRecipientMemoValueChange, - ) + val memoTextField = state.memoTextField + if (memoTextField != null) { + memoField( + memoField = memoTextField, + onMemoChange = clickIntents::onRecipientMemoValueChange, + ) + } listHeaderItem( titleRes = if (state.isAccountsMode == true) { R.string.common_accounts @@ -153,40 +156,38 @@ private fun LazyListScope.addressItem( } private fun LazyListScope.memoField( - memoField: DestinationTextFieldUM.RecipientMemo?, + memoField: DestinationTextFieldUM.RecipientMemo, onMemoChange: (String, Boolean) -> Unit, ) { - if (memoField != null) { - item(key = MEMO_FIELD_KEY) { - val placeholder = if (memoField.isEnabled) memoField.placeholder else memoField.disabledText - TextFieldWithPaste( - value = memoField.value, - label = memoField.label, - placeholder = placeholder, - footer = annotatedReference( - buildAnnotatedString { - append(stringResourceSafe(R.string.send_recipient_memo_footer_v2)) - append("\n") - withStyle(SpanStyle(fontWeight = FontWeight.Medium)) { - appendColored( - text = stringResourceSafe( - R.string.send_recipient_memo_footer_v2_highlighted, - ), - color = TangemTheme.colors.text.secondary, - ) - } - }, - ), - onValueChange = { onMemoChange(it, false) }, - onPasteClick = { onMemoChange(it, true) }, - modifier = Modifier.padding(top = 20.dp), - labelStyle = TangemTheme.typography.subtitle2, - isError = memoField.isError, - error = memoField.error, - isReadOnly = !memoField.isEnabled, - isValuePasted = memoField.isValuePasted, - ) - } + item(key = MEMO_FIELD_KEY) { + val placeholder = if (memoField.isEnabled) memoField.placeholder else memoField.disabledText + TextFieldWithPaste( + value = memoField.value, + label = memoField.label, + placeholder = placeholder, + footer = annotatedReference( + buildAnnotatedString { + append(stringResourceSafe(R.string.send_recipient_memo_footer_v2)) + append("\n") + withStyle(SpanStyle(fontWeight = FontWeight.Medium)) { + appendColored( + text = stringResourceSafe( + R.string.send_recipient_memo_footer_v2_highlighted, + ), + color = TangemTheme.colors.text.secondary, + ) + } + }, + ), + onValueChange = { onMemoChange(it, false) }, + onPasteClick = { onMemoChange(it, true) }, + modifier = Modifier.padding(top = 20.dp), + labelStyle = TangemTheme.typography.subtitle2, + isError = memoField.isError, + error = memoField.error, + isReadOnly = !memoField.isEnabled, + isValuePasted = memoField.isValuePasted, + ) } } @@ -297,8 +298,8 @@ private fun AnimateRecentAppearance(isVisible: Boolean, content: @Composable () (slideInHorizontally() + fadeIn()) .togetherWith(slideOutVertically() + fadeOut()) }, - ) { - if (it) { + ) { currentVisibility -> + if (currentVisibility) { content() } else { Box(modifier = Modifier.fillMaxWidth()) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt index 169610ae98..2d1b56c8bb 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt @@ -60,7 +60,7 @@ internal class BitcoinCustomFeeConverter( amount = feeValue, decimals = value.amount.decimals, txSize = value.txSize, - ).toString(), + )?.toString().orEmpty(), decimals = SATOSHI_DECIMALS, symbol = "", title = resourceReference(R.string.send_satoshi_per_byte_title), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt index 1467aada51..65e2574a85 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt @@ -39,14 +39,16 @@ internal class EthereumCustomFeeConverter( override fun convert(value: Fee.Ethereum): ImmutableList { return buildList { - convertFeeValue(value).let(::add) + add(convertFeeValue(value)) - when (value) { - is Fee.Ethereum.EIP1559 -> eipFeeConverter.convert(value) - is Fee.Ethereum.Legacy -> legacyFeeConverter.convert(value) - }.let(::addAll) + addAll( + when (value) { + is Fee.Ethereum.EIP1559 -> eipFeeConverter.convert(value) + is Fee.Ethereum.Legacy -> legacyFeeConverter.convert(value) + }, + ) - convertGasLimitValue(value).let(::add) + add(convertGasLimitValue(value)) } .toImmutableList() } @@ -72,8 +74,18 @@ internal class EthereumCustomFeeConverter( value: String, ): ImmutableList { return when (feeValue) { - is Fee.Ethereum.EIP1559 -> eipFeeConverter.onValueChange(feeValue, customValues, index, value) - is Fee.Ethereum.Legacy -> legacyFeeConverter.onValueChange(feeValue, customValues, index, value) + is Fee.Ethereum.EIP1559 -> eipFeeConverter.onValueChange( + feeValue = feeValue, + customValues = customValues, + index = index, + value = value, + ) + is Fee.Ethereum.Legacy -> legacyFeeConverter.onValueChange( + feeValue = feeValue, + customValues = customValues, + index = index, + value = value, + ) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt index 66607e3750..28c949d37e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt @@ -101,15 +101,16 @@ internal class KaspaCustomFeeConverter( // check that there is reveal transaction info (= krc-20 token transfer) // return without changes otherwise if (minimumFee.revealTransactionFee != null && minimumFeeAmountValue != null) { - getOrNull(FEE_AMOUNT_INDEX)?.let { - val valueDecimal = it.value.parseToBigDecimal(it.decimals) + val customFeeFieldUM = getOrNull(FEE_AMOUNT_INDEX) + if (customFeeFieldUM != null) { + val valueDecimal = customFeeFieldUM.value.parseToBigDecimal(customFeeFieldUM.decimals) // krc-20 transaction will be failed if custom fee value is less than minimum, // so we set value to minimum in this case if (valueDecimal < minimumFee.amount.value) { - val fixedValue = minimumFeeAmountValue.parseBigDecimal(it.decimals) + val fixedValue = minimumFeeAmountValue.parseBigDecimal(customFeeFieldUM.decimals) set( FEE_AMOUNT_INDEX, - it.copy( + customFeeFieldUM.copy( value = fixedValue, label = getFiatReference( rate = currencyStatus.fiatRate, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/analytics/NotificationsAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/analytics/NotificationsAnalyticEvents.kt index eeaeb2e51d..784e8dd40c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/analytics/NotificationsAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/analytics/NotificationsAnalyticEvents.kt @@ -7,7 +7,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM internal sealed class NotificationsAnalyticEvents( category: String, event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = category, event = event, params = params) { abstract val categoryName: String diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt index e79d598f5e..b44e37a21d 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt @@ -167,7 +167,7 @@ class NFTSendConfirmationNotificationsTransformerV2Test { isPrimaryButtonEnabled = true, walletName = mockk(relaxed = true), isSending = false, - showTapHelp = false, + isShowTapHelp = false, sendingFooter = mockk(relaxed = true), notifications = persistentListOf(), ) diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt index 803e634a9f..4e3050ec47 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt @@ -181,7 +181,7 @@ class SendConfirmationNotificationsTransformerV2Test { isPrimaryButtonEnabled = true, walletName = mockk(relaxed = true), isSending = false, - showTapHelp = false, + isShowTapHelp = false, sendingFooter = mockk(relaxed = true), notifications = persistentListOf(), ) diff --git a/features/staking/impl/detekt-baseline-debug.xml b/features/staking/impl/detekt-baseline-debug.xml index 920a160906..79c9fb9f11 100644 --- a/features/staking/impl/detekt-baseline-debug.xml +++ b/features/staking/impl/detekt-baseline-debug.xml @@ -5,7 +5,6 @@ BooleanPropertyNaming:AddStakingNotificationsTransformer.kt$AddStakingNotificationsTransformer$val showNotification = sendingAmount + feeAmount > balance BooleanPropertyNaming:AmountCurrencyChangeStateTransformer.kt$AmountCurrencyChangeStateTransformer$private val value: Boolean BooleanPropertyNaming:StakingModel.kt$StakingModel$val noBalanceState = balanceState == null - BooleanPropertyNaming:StakingModel.kt$StakingModel$val noYieldBalanceData = cryptoCurrencyStatus.value.yieldBalance !is YieldBalance.Data BooleanPropertyNaming:StakingUiState.kt$StakingStates.InitialInfoState.Data$val showBanner: Boolean BooleanPropertyNaming:StakingUiState.kt$StakingUiState$val showColdWalletInteractionIcon: Boolean CanBeNonNullable:StakingScreen.kt$bottomSheetConfig: TangemBottomSheetConfig? @@ -28,14 +27,6 @@ MultilineLambdaItParameter:StakingTransactionSender.kt$StakingTransactionSender${ onConstructError(it) return emptyList() } NoNameShadowing:StakingFeeTransactionLoader.kt$StakingFeeTransactionLoader$amount NoNameShadowing:StakingFeeTransactionLoader.kt$StakingFeeTransactionLoader${ if (!it.amount.isZero()) return feeResult } - NonBooleanPropertyPrefixedWithIs:AddStakingNotificationsTransformer.kt$AddStakingNotificationsTransformer$private val isAccountInitializedProvider: Provider<Boolean> - NonBooleanPropertyPrefixedWithIs:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$val isStakingEnabled = getStakingAvailabilityUseCase.invokeSync( userWalletId = selectedUserWalletId, cryptoCurrency = cryptoCurrency, ).getOrNull() - NonBooleanPropertyPrefixedWithIs:StakingFeeTransactionLoader.kt$StakingFeeTransactionLoader$private val isFeeApproximateUseCase: IsFeeApproximateUseCase - NonBooleanPropertyPrefixedWithIs:StakingModel.kt$StakingModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:StakingModel.kt$StakingModel$private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase - NonBooleanPropertyPrefixedWithIs:StakingModel.kt$StakingModel$private val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase - NonBooleanPropertyPrefixedWithIs:StakingModel.kt$StakingModel$private val isBalanceHiddenFlow: StateFlow<Boolean> field = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:StakingTransactionSender.kt$StakingTransactionSender$private val isFeeApproximateUseCase: IsFeeApproximateUseCase NullCheckOnMutableProperty:StakingModel.kt$StakingModel$if (feeCryptoCurrencyStatus != null && fee != null) { getBalanceNotEnoughForFeeWarningUseCase( fee = fee, userWalletId = userWalletId, tokenStatus = cryptoCurrencyStatus, coinStatus = feeCryptoCurrencyStatus ?: cryptoCurrencyStatus, ).getOrNull() } else { null } NullableToStringCall:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$$networkId NullableToStringCall:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$$tokenId @@ -50,7 +41,6 @@ UnnecessaryLet:StakingTosText.kt$let { onTextClick(PRIVACY_POLICY_URL) } UnnecessaryLet:StakingTosText.kt$let { onTextClick(TERMS_OF_USE_URL) } UnsafeCallOnNullableType:StakingModel.kt$StakingModel$tonAccountInitializeTransaction!! - UseOrEmpty:StakingModel.kt$StakingModel$yieldBalance?.balance?.items ?: emptyList() UseOrEmpty:StakingTransactionSender.kt$StakingTransactionSender$getExplorerTransactionUrlUseCase( txHash = transactionHashes.last(), networkId = cryptoCurrencyStatus.currency.network.id, ).getOrNull() ?: "" VarCouldBeVal:StakingModel.kt$StakingModel$private var actionsJobHolder: JobHolder = JobHolder() VarCouldBeVal:StakingModel.kt$StakingModel$private var approvalJobHolder: JobHolder = JobHolder() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 9e1aaa62ba..fd67cc140c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -190,9 +190,9 @@ internal class StakingModel @Inject constructor( private val balancesToShow: List get() { - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data + val stakeKitBalance = cryptoCurrencyStatus.value.stakingBalance as? StakingBalance.Data.StakeKit return invalidatePendingTransactionsUseCase( - balanceItems = yieldBalance?.balance?.items ?: emptyList(), + balanceItems = stakeKitBalance?.balance?.items.orEmpty(), stakingActions = stakingActions, token = yield.token, ).getOrElse { emptyList() } @@ -277,10 +277,10 @@ internal class StakingModel @Inject constructor( modelScope.launch { val isInitialInfoStep = value.currentStep == StakingStep.InitialInfo val noBalanceState = balanceState == null - val noYieldBalanceData = cryptoCurrencyStatus.value.yieldBalance !is YieldBalance.Data + val hasNoYieldBalanceData = cryptoCurrencyStatus.value.stakingBalance !is StakingBalance.Data.StakeKit when { - isInitialInfoStep && noBalanceState && yield.allValidatorsFull && noYieldBalanceData -> { + isInitialInfoStep && noBalanceState && yield.allValidatorsFull && hasNoYieldBalanceData -> { stakingEventFactory.createStakingValidatorsUnavailableAlert() return@launch } @@ -466,7 +466,7 @@ internal class StakingModel @Inject constructor( } override fun onInitialInfoBannerClick() { - analyticsEventHandler.send(StakingAnalyticsEvent.WhatIsStaking) + analyticsEventHandler.send(StakingAnalyticsEvent.WhatIsStaking()) innerRouter.openUrl(WHAT_IS_STAKING_ARTICLE_URL) } @@ -512,7 +512,7 @@ internal class StakingModel @Inject constructor( } override fun onMaxValueClick() { - analyticsEventHandler.send(StakingAnalyticsEvent.ButtonMax) + analyticsEventHandler.send(StakingAnalyticsEvent.ButtonMax()) stateController.update( AmountMaxValueStateTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, @@ -554,7 +554,7 @@ internal class StakingModel @Inject constructor( } override fun openRewardsValidators() { - analyticsEventHandler.send(StakingAnalyticsEvent.ButtonRewards) + analyticsEventHandler.send(StakingAnalyticsEvent.ButtonRewards()) val rewardsValidators = stateController.value.rewardsValidatorsState as? StakingStates.RewardsValidatorsState.Data @@ -879,7 +879,7 @@ internal class StakingModel @Inject constructor( } override fun onExploreClick() { - analyticsEventHandler.send(StakingAnalyticsEvent.ButtonExplore) + analyticsEventHandler.send(StakingAnalyticsEvent.ButtonExplore()) val confirmationDataState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content val txUrl = transactionDoneState?.txUrl @@ -893,7 +893,7 @@ internal class StakingModel @Inject constructor( val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content val txUrl = transactionDoneState?.txUrl - analyticsEventHandler.send(StakingAnalyticsEvent.ButtonShare) + analyticsEventHandler.send(StakingAnalyticsEvent.ButtonShare()) if (txUrl != null) { vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) shareManager.shareText(txUrl) @@ -1117,7 +1117,7 @@ internal class StakingModel @Inject constructor( private suspend fun onDataLoaded(status: CryptoCurrencyStatus) { if (!isInitialInfoAnalyticSent) { isInitialInfoAnalyticSent = true - val balances = status.value.yieldBalance as? YieldBalance.Data + val balances = status.value.stakingBalance as? StakingBalance.Data.StakeKit paramsInterceptorHolder.addParamsInterceptor( interceptor = StakingParamsInterceptor(status.currency.symbol), ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 673dc355c0..294890807d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -81,12 +81,12 @@ internal class StakingStateRouter( } fun showRewardsValidators() { - analyticsEventsHandler.send(StakingAnalyticsEvent.RewardScreenOpened) + analyticsEventsHandler.send(StakingAnalyticsEvent.RewardScreenOpened()) stateController.update { it.copy(currentStep = StakingStep.RewardsValidators) } } private fun showAmount() { - analyticsEventsHandler.send(StakingAnalyticsEvent.AmountScreenOpened) + analyticsEventsHandler.send(StakingAnalyticsEvent.AmountScreenOpened()) stateController.update { it.copy(currentStep = StakingStep.Amount) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt index 5d548f7ab4..20230aecef 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt @@ -10,7 +10,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.BalanceItem import com.tangem.domain.models.staking.BalanceType import com.tangem.domain.models.staking.BalanceType.Companion.isClickable -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.utils.getRewardStakingBalance @@ -77,11 +77,11 @@ internal class BalanceItemConverter( val isIncludeStakingTotalBalance = BlockchainUtils.isIncludeStakingTotalBalance( blockchainId = cryptoCurrencyStatus.currency.network.rawId, ) - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data + val stakeKitBalance = cryptoCurrencyStatus.value.stakingBalance as? StakingBalance.Data.StakeKit return if (isIncludeStakingTotalBalance) { amount } else { - amount - yieldBalance?.getRewardStakingBalance().orZero() + amount - stakeKitBalance?.getRewardStakingBalance().orZero() } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index 4c47f26565..68e09c30c5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -9,7 +9,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.BalanceItem import com.tangem.domain.models.staking.BalanceType -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates @@ -24,9 +24,9 @@ internal class RewardsValidatorStateConverter( private val yield: Yield, ) : Converter { override fun convert(value: Unit): StakingStates.RewardsValidatorsState { - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance - return if (yieldBalance is YieldBalance.Data) { - val balances = yieldBalance.balance.items + val stakingBalance = cryptoCurrencyStatus.value.stakingBalance + return if (stakingBalance is StakingBalance.Data.StakeKit) { + val balances = stakingBalance.balance.items StakingStates.RewardsValidatorsState.Data( isPrimaryButtonEnabled = true, rewards = balances @@ -35,6 +35,7 @@ internal class RewardsValidatorStateConverter( .toPersistentList(), ) } else { + // TODO p2p StakingStates.RewardsValidatorsState.Empty() } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 75c557eb38..8c118c5572 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -9,7 +9,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.BalanceItem import com.tangem.domain.models.staking.BalanceType import com.tangem.domain.models.staking.RewardBlockType -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.utils.getRewardStakingBalance @@ -36,11 +36,11 @@ internal class YieldBalancesConverter( val appCurrency = appCurrencyProvider() val cryptoCurrency = cryptoCurrencyStatus.currency - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data + val stakeKitBalance = cryptoCurrencyStatus.value.stakingBalance as? StakingBalance.Data.StakeKit val balanceToShowItems = balancesToShowProvider() - return if (yieldBalance != null || balanceToShowItems.any { it.isPending }) { - val cryptoRewardsValue = yieldBalance?.getRewardStakingBalance() + return if (stakeKitBalance != null || balanceToShowItems.any { it.isPending }) { + val cryptoRewardsValue = stakeKitBalance?.getRewardStakingBalance() val fiatRate = cryptoCurrencyStatus.value.fiatRate val fiatRewardsValue = if (fiatRate != null && cryptoRewardsValue != null) { @@ -49,13 +49,13 @@ internal class YieldBalancesConverter( null } val type = getRewardBlockType() - val pendingRewardsConstraints = yieldBalance?.balance?.items + val pendingRewardsConstraints = stakeKitBalance?.balance?.items ?.firstOrNull { it.type == BalanceType.REWARDS } ?.pendingActionsConstraints ?.firstOrNull { it.type == StakingActionType.CLAIM_REWARDS } InnerYieldBalanceState.Data( - integrationId = yieldBalance?.stakingId?.integrationId, + integrationId = stakeKitBalance?.stakingId?.integrationId, reward = YieldReward( rewardsCrypto = cryptoRewardsValue.format { crypto(cryptoCurrency) }, rewardsFiat = fiatRewardsValue.format { @@ -71,6 +71,7 @@ internal class YieldBalancesConverter( balances = balanceToShowItems.mapBalances(), ) } else { + // TODO p2p InnerYieldBalanceState.Empty } } @@ -84,8 +85,8 @@ internal class YieldBalancesConverter( private fun getRewardBlockType(): RewardBlockType { val blockchainId = cryptoCurrencyStatus.currency.network.rawId - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data - val rewards = yieldBalance?.balance?.items + val stakeKitBalance = cryptoCurrencyStatus.value.stakingBalance as? StakingBalance.Data.StakeKit + val rewards = stakeKitBalance?.balance?.items ?.filter { it.type == BalanceType.REWARDS && !it.amount.isZero() } val isActionable = rewards?.any { it.pendingActions.isNotEmpty() } == true diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index 73b8c296f2..30f437ca80 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -35,7 +35,7 @@ internal class SetConfirmationStateAssentTransformer( isFeeApproximate = isFeeApproximate, ), isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) { - sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + sources.stakingBalanceSource.isActual() && sources.networkSource.isActual() }, ) } else { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt index 6f745765ff..8b650155fb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -26,7 +26,7 @@ internal class SetConfirmationStateCompletedTransformer( return if (this is StakingStates.ConfirmationState.Data) { copy( isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) { - sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + sources.stakingBalanceSource.isActual() && sources.networkSource.isActual() }, innerState = InnerConfirmationStakingState.COMPLETED, footerText = TextReference.EMPTY, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt index db0da4876d..16ff686c06 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt @@ -15,7 +15,7 @@ internal class SetConfirmationStateResetAssentTransformer( confirmationState = if (confirmationState is StakingStates.ConfirmationState.Data) { confirmationState.copy( isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) { - sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + sources.stakingBalanceSource.isActual() && sources.networkSource.isActual() }, innerState = InnerConfirmationStakingState.ASSENT, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index e9e9fbe02b..2f55f146f8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -90,7 +90,7 @@ internal class SetInitialDataStateTransformer( val status = cryptoCurrencyStatus.value return StakingStates.InitialInfoState.Data( isPrimaryButtonEnabled = with(status) { - !amount.isNullOrZero() && sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + !amount.isNullOrZero() && sources.stakingBalanceSource.isActual() && sources.networkSource.isActual() }, showBanner = !isAnyTokenStaked && yieldBalance == InnerYieldBalanceState.Empty, infoItems = getInfoItems(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt index 235472ef08..da0c0d9a9a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt @@ -37,7 +37,7 @@ internal class SetConfirmationStateAssentApprovalTransformer( isFeeApproximate = false, ), isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) { - sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + sources.stakingBalanceSource.isActual() && sources.networkSource.isActual() }, isApprovalNeeded = true, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt index 66d3c56eac..3194f4e9ae 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt @@ -127,7 +127,7 @@ internal class AddStakingNotificationsTransformer( } val isActualSources = with(cryptoCurrencyStatus.value) { - sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + sources.stakingBalanceSource.isActual() && sources.networkSource.isActual() } return prevState.copy( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt index 8959e90124..bdda0a7ac5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt @@ -6,7 +6,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.BalanceType -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType @@ -134,7 +134,7 @@ internal class StakingInfoNotificationsFactory( private fun MutableList.addTronRevoteNotification() { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val isTron = isTron(cryptoCurrencyStatus.currency.network.rawId) - val hasStakedBalance = (cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data)?.balance + val hasStakedBalance = (cryptoCurrencyStatus.value.stakingBalance as? StakingBalance.Data.StakeKit)?.balance ?.items?.any { it.type == BalanceType.PREPARING || it.type == BalanceType.STAKED || diff --git a/features/swap-v2/impl/detekt-baseline-debug.xml b/features/swap-v2/impl/detekt-baseline-debug.xml deleted file mode 100644 index 3bc57815b3..0000000000 --- a/features/swap-v2/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - BooleanPropertyNaming:ConfirmUM.kt$ConfirmUM.Content$val showTapHelp: Boolean - BooleanPropertyNaming:SwapAmountModel.kt$SwapAmountModel$private var showBestRateAnimation: Boolean = false - BooleanPropertyNaming:SwapAmountModel.kt$SwapAmountModel$val showSendViaSwapNotification = shouldShowNotificationUseCase( NotificationId.SendViaSwapTokenSelectorNotification.key, ) - BooleanPropertyNaming:SwapAmountPrimaryReadyStateTransformer.kt$SwapAmountPrimaryReadyStateTransformer$private val showBestRateAnimation: Boolean - BooleanPropertyNaming:SwapAmountSecondaryReadyStateTransformer.kt$SwapAmountSecondaryReadyStateTransformer$private val showBestRateAnimation: Boolean - BooleanPropertyNaming:SwapAmountSelectQuoteTransformer.kt$SwapAmountSelectQuoteTransformer$private val needApplyFCARestrictions: Boolean - BooleanPropertyNaming:SwapAmountSetQuotesTransformer.kt$SwapAmountSetQuotesTransformer$private val needApplyFcaRestrictions: Boolean - BooleanPropertyNaming:SwapAmountUM.kt$SwapAmountUM.Content$val showBestRateAnimation: Boolean - BooleanPropertyNaming:SwapAmountUM.kt$SwapAmountUM.Content$val showFCAWarning: Boolean - BooleanPropertyNaming:SwapChooseProviderModel.kt$SwapChooseProviderModel$private val needApplyFCARestrictions = params.userCountry.needApplyFCARestrictions() - BooleanPropertyNaming:SwapProviderListItemConverter.kt$SwapProviderListItemConverter$private val needApplyFCARestrictions: Boolean - BooleanPropertyNaming:SwapProviderStateConverter.kt$SwapProviderStateConverter$private val needApplyFCARestrictions: Boolean - CouldBeSequence:SwapAmountModel.kt$SwapAmountModel$filter { it.currencyStatus.currency.id == toCryptoCurrency.id } - MaxChainedCallsOnSameLine:SwapAlertFactory.kt$SwapAlertFactory$confirmData?.fee?.amount?.value?.toString().orEmpty() - MultilineLambdaItParameter:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel${ it.copy( confirmUM = SendWithSwapConfirmInitialStateTransformer( isShowTapHelp = isShowTapHelp, ).transform(uiState.value.confirmUM), ) } - MultilineLambdaItParameter:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel${ val confirmUM = it.confirmUM as? ConfirmUM.Content it.copy(confirmUM = confirmUM?.copy(showTapHelp = showTapHelp) ?: it.confirmUM) } - MultilineLambdaItParameter:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel${ val feeUM = it.feeSelectorUM as? FeeSelectorUM.Content it.copy( confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( isPrimaryButtonEnabled = !hasError && feeUM != null, ) ?: it.confirmUM, ) } - MultilineLambdaItParameter:SendWithSwapConfirmationNotificationsTransformer.kt$SendWithSwapConfirmationNotificationsTransformer${ ConfirmUM.Content.LegalUM( title = resourceReference(R.string.common_privacy_policy), link = it, ) } - MultilineLambdaItParameter:SendWithSwapConfirmationNotificationsTransformer.kt$SendWithSwapConfirmationNotificationsTransformer${ ConfirmUM.Content.LegalUM( title = resourceReference(R.string.common_terms_of_use), link = it, ) } - MultilineLambdaItParameter:SendWithSwapModel.kt$SendWithSwapModel${ Timber.w(it.toString()) swapAlertFactory.getGenericErrorState( expressError = ExpressError.UnknownError, onFailedTxEmailClick = { modelScope.launch { swapAlertFactory.onFailedTxEmailClick( userWallet = userWallet, cryptoCurrency = params.currency, errorMessage = it.toString(), ) } }, popBack = ::onBackClick, ) } - MultilineLambdaItParameter:SendWithSwapModel.kt$SendWithSwapModel${ it.copy( destinationUM = DestinationUM.Empty(), feeSelectorUM = FeeSelectorUM.Loading, confirmUM = ConfirmUM.Empty, navigationUM = NavigationUM.Empty, ) } - MultilineLambdaItParameter:SwapAmountSetQuotesTransformer.kt$SwapAmountSetQuotesTransformer${ it is SwapQuoteUM.Content || it is SwapQuoteUM.Allowance || (it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError } - MultilineLambdaItParameter:SwapChooseProviderModel.kt$SwapChooseProviderModel${ it is SwapQuoteUM.Content || it is SwapQuoteUM.Allowance || (it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError } - MultilineLambdaItParameter:SwapChooseTokenNetworkModel.kt$SwapChooseTokenNetworkModel${ Timber.e("Failed to get user wallet: $it") swapChooseTokenAlertFactory.getGenericErrorState(params.onDismiss) return } - MultilineLambdaItParameter:SwapChooseTokenNetworkModel.kt$SwapChooseTokenNetworkModel${ Timber.e(it.toString()) uiState.update( SwapChooseErrorStateTransformer( tokenName = params.token.name, onDismiss = params.onDismiss, ), ) return@launch } - MultilineLambdaItParameter:SwapTransactionSender.kt$SwapTransactionSender${ Timber.e(it, "Failed to create swap CEX tx data") onSendError(SendTransactionError.UnknownError(Exception(it))) return } - MultilineLambdaItParameter:SwapTransactionSender.kt$SwapTransactionSender${ onExpressError(it); return } - NamedArguments:SwapTransactionSender.kt$SwapTransactionSender$getSwapDataUseCase( userWallet = userWallet, fromCryptoCurrencyStatus = fromStatus, fromAmount = fromAmount.toStringWithRightOffset(fromStatus.currency.decimals), toCryptoCurrency = toStatus.currency, toAddress = destination, expressProvider = provider, rateType = rateType, expressOperationType, ) - NoNameShadowing:SendWithSwapContent.kt$navigationUM - NoNameShadowing:SwapAmountContent.kt$amountFieldUM - NonBooleanPropertyPrefixedWithIs:SendWithSwapConfirmComponent.kt$SendWithSwapConfirmComponent.Params$val isAccountModeFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendWithSwapConfirmComponent.kt$SendWithSwapConfirmComponent.Params$val isBalanceHidingFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel$private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase - NonBooleanPropertyPrefixedWithIs:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel$private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase - NonBooleanPropertyPrefixedWithIs:SendWithSwapModel.kt$SendWithSwapModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:SendWithSwapModel.kt$SendWithSwapModel$val isAccountModeFlow: StateFlow<Boolean> field = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:SendWithSwapModel.kt$SendWithSwapModel$val isBalanceHiddenFlow: StateFlow<Boolean> field = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:SwapAmountComponentParams.kt$SwapAmountComponentParams$abstract val isAccountModeFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SwapAmountComponentParams.kt$SwapAmountComponentParams$abstract val isBalanceHidingFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SwapAmountComponentParams.kt$SwapAmountComponentParams.AmountBlockParams$override val isAccountModeFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SwapAmountComponentParams.kt$SwapAmountComponentParams.AmountBlockParams$override val isBalanceHidingFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SwapAmountComponentParams.kt$SwapAmountComponentParams.AmountParams$override val isAccountModeFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:SwapAmountComponentParams.kt$SwapAmountComponentParams.AmountParams$override val isBalanceHidingFlow: StateFlow<Boolean> - NullableToStringCall:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel$error.toString() - NullableToStringCall:SwapAmountModel.kt$SwapAmountModel$$primaryStatus - NullableToStringCall:SwapAmountModel.kt$SwapAmountModel$$secondaryStatus - PropertyUsedBeforeDeclaration:SendWithSwapConfirmModel.kt$SendWithSwapConfirmModel$amountUM - UnnecessaryEventHandlerParameter:SwapAmountContent.kt$onExpandEditField: (SwapAmountType) -> Unit - UseEmptyCounterpart:SendWithSwapAnalyticEvents.kt$SendWithSwapAnalyticEvents$mapOf() - UseEmptyCounterpart:SendWithSwapSuccessContent.kt$listOf() - UseEmptyCounterpart:SwapAmountAnalyticEvents.kt$SwapAmountAnalyticEvents$mapOf() - VarCouldBeVal:SwapAmountModel.kt$SwapAmountModel$private var primaryCryptoCurrency: CryptoCurrency = params.primaryCryptoCurrencyStatusFlow.value.currency - VarCouldBeVal:SwapAmountModel.kt$SwapAmountModel$private var userWallet = params.userWallet - - diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticEvents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticEvents.kt index 197bd8a27c..4dd7c9562d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticEvents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticEvents.kt @@ -6,7 +6,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER internal sealed class SwapAmountAnalyticEvents( category: String, event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = category, event = event, params = params) { data class ProviderSelectorClicked( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt index 62eb8a0d3b..4cbc2867a6 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt @@ -48,11 +48,11 @@ internal sealed class SwapAmountUM { val swapCurrencies: SwapCurrencies, val swapQuotes: ImmutableList, val selectedQuote: SwapQuoteUM, - val showFCAWarning: Boolean, + val isShowFCAWarning: Boolean, // extra data val appCurrency: AppCurrency?, - val showBestRateAnimation: Boolean, + val isShowBestRateAnimation: Boolean, ) : SwapAmountUM() } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index da46d20b3f..8546a9c6e2 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -26,11 +26,8 @@ import com.tangem.domain.notifications.ShouldShowNotificationUseCase import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions -import com.tangem.domain.swap.models.SwapCurrencies -import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.swap.models.* import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection -import com.tangem.domain.swap.models.SwapQuoteModel -import com.tangem.domain.swap.models.getGroupWithDirection import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase import com.tangem.domain.swap.usecase.SelectInitialPairUseCase import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase @@ -99,9 +96,9 @@ internal class SwapAmountModel @Inject constructor( private val params: SwapAmountComponentParams = paramsContainer.require() private val swapDirection = params.swapDirection private var appCurrency = AppCurrency.Default - private var userWallet = params.userWallet + private val userWallet = params.userWallet - private var primaryCryptoCurrency: CryptoCurrency = params.primaryCryptoCurrencyStatusFlow.value.currency + private val primaryCryptoCurrency: CryptoCurrency = params.primaryCryptoCurrencyStatusFlow.value.currency private var primaryMaximumAmountBoundary: EnterAmountBoundary by Delegates.notNull() private var primaryMinimumAmountBoundary: EnterAmountBoundary by Delegates.notNull() @@ -111,7 +108,7 @@ internal class SwapAmountModel @Inject constructor( private var userCountry: UserCountry = UserCountry.Other(Locale.getDefault().country) val bottomSheetNavigation: SlotNavigation = SlotNavigation() - private var showBestRateAnimation: Boolean = false + private var isShowBestRateAnimation: Boolean = false val uiState: StateFlow field = MutableStateFlow(params.amountUM) @@ -124,7 +121,7 @@ internal class SwapAmountModel @Inject constructor( appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } userCountry = getUserCountryUseCase.invokeSync().getOrNull() ?: UserCountry.Other(Locale.getDefault().country) - showBestRateAnimation = swapBestRateAnimationStore.getSyncOrNull() + isShowBestRateAnimation = swapBestRateAnimationStore.getSyncOrNull() } configAmountNavigation() subscribeOnCryptoCurrencyStatusFlow() @@ -169,7 +166,7 @@ internal class SwapAmountModel @Inject constructor( quoteUM = quoteUM, secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, - needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), + isNeedApplyFCARestrictions = userCountry.needApplyFCARestrictions(), ), ) } @@ -270,7 +267,7 @@ internal class SwapAmountModel @Inject constructor( override fun onSelectTokenClick() { val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return modelScope.launch { - val showSendViaSwapNotification = shouldShowNotificationUseCase( + val isShowSendViaSwapNotification = shouldShowNotificationUseCase( NotificationId.SendViaSwapTokenSelectorNotification.key, ) val isEditMode = amountParams.currentRoute.firstOrNull()?.isEditMode == true @@ -281,7 +278,7 @@ internal class SwapAmountModel @Inject constructor( initialCurrency = primaryCryptoCurrency, selectedCurrency = selectedCurrency.takeIf { isEditMode }, source = AppRoute.ChooseManagedTokens.Source.SendViaSwap, - shouldShowSendViaSwapNotification = showSendViaSwapNotification, + shouldShowSendViaSwapNotification = isShowSendViaSwapNotification, analyticsCategoryName = params.analyticsCategoryName, ), ) @@ -306,7 +303,7 @@ internal class SwapAmountModel @Inject constructor( fun onFinishAnimation() { uiState.update { - (it as? SwapAmountUM.Content)?.copy(showBestRateAnimation = false) ?: it + (it as? SwapAmountUM.Content)?.copy(isShowBestRateAnimation = false) ?: it } } @@ -367,7 +364,7 @@ internal class SwapAmountModel @Inject constructor( swapDirection = swapDirection, clickIntents = this, isBalanceHidden = params.isBalanceHidingFlow.value, - showBestRateAnimation = showBestRateAnimation, + isShowBestRateAnimation = isShowBestRateAnimation, isSingleWallet = isOnlyOneWallet, isAccountsMode = params.isAccountModeFlow.value, account = params.accountFlow.value, @@ -418,7 +415,7 @@ internal class SwapAmountModel @Inject constructor( swapDirection = swapDirection, clickIntents = this, isBalanceHidden = params.isBalanceHidingFlow.value, - showBestRateAnimation = showBestRateAnimation, + isShowBestRateAnimation = isShowBestRateAnimation, isSingleWallet = isOnlyOneWallet, isAccountsMode = isAccountsMode, account = account, @@ -532,7 +529,7 @@ internal class SwapAmountModel @Inject constructor( swapDirection = swapDirection, clickIntents = this@SwapAmountModel, isBalanceHidden = params.isBalanceHidingFlow.value, - showBestRateAnimation = showBestRateAnimation, + isShowBestRateAnimation = isShowBestRateAnimation, isSingleWallet = isOnlyOneWallet, isAccountsMode = params.isAccountModeFlow.value, account = params.accountFlow.value, @@ -540,6 +537,7 @@ internal class SwapAmountModel @Inject constructor( ) startLoadingQuotesTask(isSilentReload = false) } else { + @Suppress("NullableToStringCall") Timber.e( """ Invalid cryptocurrencies status: @@ -596,50 +594,50 @@ internal class SwapAmountModel @Inject constructor( val isAmountScreen = params is SwapAmountComponentParams.AmountParams val isAmountError = fromAmount?.amountTextField?.isError == true || fromAmountValue.isNullOrZero() if (isAmountScreen && isAmountError) { - uiState.transformerUpdate(SwapQuoteEmptyStateTransformer) - return + uiState.transformerUpdate(SwapQuoteEmptyStateTransformer); return } if (!isSilentReload) uiState.transformerUpdate(SwapQuoteLoadingStateTransformer) modelScope.launch { - val quotes = state.swapCurrencies.getGroupWithDirection(state.swapDirection).available.filter { - it.currencyStatus.currency.id == toCryptoCurrency.id - }.flatMap { - it.providers - }.map { provider -> - async { - getSwapQuoteUseCase( - userWallet = userWallet, - fromCryptoCurrency = fromCryptoCurrency, - toCryptoCurrency = toCryptoCurrency, - fromAmount = fromAmountValue, - provider = provider, - ).fold( - ifLeft = { error -> - SwapQuoteUM.Error( - provider = provider, - expressError = error, - ) - }, - ifRight = { quote: SwapQuoteModel -> - SwapQuoteUMConverter( - primaryCurrency = fromCryptoCurrency, - secondaryCurrency = toCryptoCurrency, - swapDirection = swapDirection, - allowanceContract = quote.allowanceContract, - isApprovalNeeded = checkAllowance(state, quote), - fromAmount = fromAmountValue, - ).convert( - SwapQuoteUMConverter.Data( - quote = quote, + val quotes = state.swapCurrencies.getGroupWithDirection(state.swapDirection).available + .asSequence() + .filter { swapCurrencyStatus -> swapCurrencyStatus.currencyStatus.currency.id == toCryptoCurrency.id } + .flatMap(SwapCryptoCurrency::providers) + .toList() + .map { provider -> + async { + getSwapQuoteUseCase( + userWallet = userWallet, + fromCryptoCurrency = fromCryptoCurrency, + toCryptoCurrency = toCryptoCurrency, + fromAmount = fromAmountValue, + provider = provider, + ).fold( + ifLeft = { error -> + SwapQuoteUM.Error( provider = provider, - ), - ) - }, - ) - } - }.awaitAll() + expressError = error, + ) + }, + ifRight = { quote: SwapQuoteModel -> + SwapQuoteUMConverter( + primaryCurrency = fromCryptoCurrency, + secondaryCurrency = toCryptoCurrency, + swapDirection = swapDirection, + allowanceContract = quote.allowanceContract, + isApprovalNeeded = checkAllowance(state, quote), + fromAmount = fromAmountValue, + ).convert( + SwapQuoteUMConverter.Data( + quote = quote, + provider = provider, + ), + ) + }, + ) + } + }.awaitAll() uiState.transformerUpdate( SwapAmountSetQuotesTransformer( @@ -647,7 +645,7 @@ internal class SwapAmountModel @Inject constructor( secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, isSilentReload = isSilentReload, - needApplyFcaRestrictions = userCountry.needApplyFCARestrictions(), + isNeedApplyFcaRestrictions = userCountry.needApplyFCARestrictions(), ), ) feeSelectorReloadTrigger.triggerUpdate() diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt index 9d39c5e8f4..ca25cc3fda 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt @@ -24,7 +24,7 @@ internal class SwapAmountPrimaryReadyStateTransformer( private val clickIntents: AmountScreenClickIntents, private val swapDirection: SwapDirection, private val isBalanceHidden: Boolean, - private val showBestRateAnimation: Boolean, + private val isShowBestRateAnimation: Boolean, private val isSingleWallet: Boolean, private val isAccountsMode: Boolean, private val account: Account.CryptoPortfolio?, @@ -60,8 +60,8 @@ internal class SwapAmountPrimaryReadyStateTransformer( swapQuotes = persistentListOf(), selectedQuote = SwapQuoteUM.Empty, appCurrency = appCurrency, - showBestRateAnimation = showBestRateAnimation, - showFCAWarning = false, + isShowBestRateAnimation = isShowBestRateAnimation, + isShowFCAWarning = false, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt index 7d4a258917..298b11ad75 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt @@ -25,7 +25,7 @@ internal class SwapAmountSecondaryReadyStateTransformer( private val clickIntents: AmountScreenClickIntents, private val swapDirection: SwapDirection, private val isBalanceHidden: Boolean, - private val showBestRateAnimation: Boolean, + private val isShowBestRateAnimation: Boolean, private val isSingleWallet: Boolean, private val isAccountsMode: Boolean, private val account: Account.CryptoPortfolio?, @@ -59,8 +59,8 @@ internal class SwapAmountSecondaryReadyStateTransformer( swapQuotes = persistentListOf(), selectedQuote = SwapQuoteUM.Empty, appCurrency = appCurrency, - showBestRateAnimation = showBestRateAnimation, - showFCAWarning = false, + isShowBestRateAnimation = isShowBestRateAnimation, + isShowFCAWarning = false, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt index cd0df36880..03058c869b 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt @@ -19,7 +19,7 @@ internal class SwapAmountSelectQuoteTransformer( private val quoteUM: SwapQuoteUM, private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, - private val needApplyFCARestrictions: Boolean, + private val isNeedApplyFCARestrictions: Boolean, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState @@ -31,7 +31,7 @@ internal class SwapAmountSelectQuoteTransformer( return prevState.copy( isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content, selectedQuote = quoteUM, - showFCAWarning = needApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true, + isShowFCAWarning = isNeedApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true, primaryAmount = if (prevState.selectedAmountType == SwapAmountType.From) { val swapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content val amountField = swapAmountField?.amountField as? AmountState.Data diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index 941fa7e8ec..cd19623928 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -22,14 +22,14 @@ internal class SwapAmountSetQuotesTransformer( private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, private val isSilentReload: Boolean, - private val needApplyFcaRestrictions: Boolean, + private val isNeedApplyFcaRestrictions: Boolean, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState - val isSingleProvider = quotes.filter { - it is SwapQuoteUM.Content || it is SwapQuoteUM.Allowance || - (it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError + val isSingleProvider = quotes.filter { swapQuoteUM -> + swapQuoteUM is SwapQuoteUM.Content || swapQuoteUM is SwapQuoteUM.Allowance || + (swapQuoteUM as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError }.isSingleItem() val sortedQuotes = quotes.sortedWith(SwapQuotesComparator) @@ -49,7 +49,7 @@ internal class SwapAmountSetQuotesTransformer( quoteUM = selectedQuote, secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, - needApplyFCARestrictions = needApplyFcaRestrictions && + isNeedApplyFCARestrictions = isNeedApplyFcaRestrictions && selectedQuote.provider?.isRestrictedByFCA() == true, ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index 5fc6dc2696..008e402a09 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -88,7 +88,7 @@ internal fun SwapAmountBlockContent( SwapChooseProviderContent( isBestRate = isBestRate, isSingleProvider = quoteContent?.isSingleProvider == true, - showBestRateAnimation = amountUM.showBestRateAnimation, + showBestRateAnimation = amountUM.isShowBestRateAnimation, expressProvider = amountUM.selectedQuote.provider, onClick = onProviderSelectClick, onFinishAnimation = onFinishAnimation, @@ -98,7 +98,7 @@ internal fun SwapAmountBlockContent( start.linkTo(parent.start) end.linkTo(parent.end) }, - showFCAWarning = amountUM.showFCAWarning, + showFCAWarning = amountUM.isShowFCAWarning, ) } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt index 91c6f69e30..61ec2095f4 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt @@ -175,7 +175,7 @@ private fun SwapAmountBlock( selectedQuote = selectedQuote, isSelectedAmountType = isSelectedAmountType, isFixedRate = isFixedRate, - onExpandEditField = clickIntents::onExpandEditField, + onExpandEditField = { clickIntents.onExpandEditField(amountFieldUM.amountType) }, onSelectTokenClick = clickIntents::onSelectTokenClick, onMaxAmountClick = clickIntents::onMaxValueClick, ) @@ -195,14 +195,14 @@ private fun SwapAmountEditBlock( verticalArrangement = Arrangement.spacedBy(12.dp), modifier = modifier.padding(top = 48.dp, bottom = 28.dp), ) { - when (val amountFieldUM = amountFieldUM.amountField) { + when (val amountField = amountFieldUM.amountField) { !is AmountState.Data -> { TextShimmer( style = TangemTheme.typography.caption2, modifier = Modifier.width(60.dp), ) } - else -> AccountTitle(amountFieldUM.accountTitleUM) + else -> AccountTitle(amountField.accountTitleUM) } AmountFieldV2( amountUM = amountFieldUM.amountField, @@ -221,7 +221,7 @@ private fun SwapAmountInfo( selectedQuote: SwapQuoteUM?, isSelectedAmountType: Boolean, isFixedRate: Boolean, - onExpandEditField: (SwapAmountType) -> Unit, + onExpandEditField: () -> Unit, onMaxAmountClick: () -> Unit, onSelectTokenClick: () -> Unit, modifier: Modifier = Modifier, @@ -237,7 +237,7 @@ private fun SwapAmountInfo( enabled = (amountFieldUM as? SwapAmountFieldUM.Content)?.isClickEnabled == true, onClick = { if (isFixedRate) { - onExpandEditField(amountFieldUM.amountType) + onExpandEditField() } else { onSelectTokenClick() } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index 01c4c02b06..798b7fd4d0 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -90,8 +90,8 @@ internal data object SwapAmountContentPreview { secondaryCryptoCurrencyStatus = cryptoCurrencyStatus, swapRateType = ExpressRateType.Float, appCurrency = AppCurrency.Default, - showBestRateAnimation = false, - showFCAWarning = false, + isShowBestRateAnimation = false, + isShowFCAWarning = false, ) val defaultState = SwapAmountUM.Content( @@ -129,8 +129,8 @@ internal data object SwapAmountContentPreview { secondaryCryptoCurrencyStatus = cryptoCurrencyStatus, swapRateType = ExpressRateType.Float, isPrimaryButtonEnabled = true, - showBestRateAnimation = false, - showFCAWarning = true, + isShowBestRateAnimation = false, + isShowFCAWarning = true, ) val defaultStateAccount: SwapAmountUM.Content diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt index 0ac41aea88..dc784cc913 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt @@ -25,13 +25,13 @@ internal class SwapChooseProviderModel @Inject constructor( private val params: SwapChooseProviderComponent.Params = paramsContainer.require() - private val needApplyFCARestrictions = params.userCountry.needApplyFCARestrictions() + private val isNeedApplyFCARestrictions = params.userCountry.needApplyFCARestrictions() private val swapProviderListItemConverter by lazy(LazyThreadSafetyMode.NONE) { SwapProviderListItemConverter( cryptoCurrency = params.cryptoCurrency, selectedProvider = params.selectedProvider, - needApplyFCARestrictions = needApplyFCARestrictions, + isNeedApplyFCARestrictions = isNeedApplyFCARestrictions, needBestRateBadge = params.providers.filterIsInstance().isSingleItem().not(), ) } @@ -45,13 +45,13 @@ internal class SwapChooseProviderModel @Inject constructor( } private fun getInitialState(): SwapChooseProviderBottomSheetContent { - val filteredProviderList = params.providers.filter { - it is SwapQuoteUM.Content || - it is SwapQuoteUM.Allowance || - (it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError + val filteredProviderList = params.providers.filter { swapQuoteUM -> + swapQuoteUM is SwapQuoteUM.Content || + swapQuoteUM is SwapQuoteUM.Allowance || + (swapQuoteUM as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError } return SwapChooseProviderBottomSheetContent( - isApplyFCARestrictions = needApplyFCARestrictions && params.selectedProvider.isRestrictedByFCA(), + isApplyFCARestrictions = isNeedApplyFCARestrictions && params.selectedProvider.isRestrictedByFCA(), providerList = swapProviderListItemConverter.convertList(filteredProviderList) .filterNotNull() .toPersistentList(), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt index 045d882f8f..56c7a0f93a 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt @@ -21,14 +21,14 @@ import com.tangem.utils.converter.Converter internal class SwapProviderListItemConverter( private val cryptoCurrency: CryptoCurrency, private val selectedProvider: ExpressProvider, - private val needApplyFCARestrictions: Boolean, + private val isNeedApplyFCARestrictions: Boolean, needBestRateBadge: Boolean, ) : Converter { private val providerStateConverter = SwapProviderStateConverter( cryptoCurrency = cryptoCurrency, selectedProvider = selectedProvider, - needApplyFCARestrictions = needApplyFCARestrictions, + isNeedApplyFCARestrictions = isNeedApplyFCARestrictions, isNeedBestRateBadge = needBestRateBadge, ) @@ -78,7 +78,7 @@ internal class SwapProviderListItemConverter( }, ) } - is SwapQuoteUM.Content -> if (needApplyFCARestrictions && value.provider.isRestrictedByFCA()) { + is SwapQuoteUM.Content -> if (isNeedApplyFCARestrictions && value.provider.isRestrictedByFCA()) { ProviderChooseUM.ExtraUM.Action( text = resourceReference(R.string.express_provider_fca_warning_list), ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt index 317f0d733f..54f3030f4f 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt @@ -20,7 +20,7 @@ internal class SwapProviderStateConverter( private val cryptoCurrency: CryptoCurrency, private val selectedProvider: ExpressProvider, private val isNeedBestRateBadge: Boolean, - private val needApplyFCARestrictions: Boolean, + private val isNeedApplyFCARestrictions: Boolean, ) : Converter { override fun convert(value: SwapQuoteUM): SwapProviderState { @@ -41,7 +41,7 @@ internal class SwapProviderStateConverter( } val additionalBadge = when { - needApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList + isNeedApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList isNeedBestRateBadge && isBestRate -> AdditionalBadge.BestTrade else -> AdditionalBadge.Empty } @@ -59,7 +59,7 @@ internal class SwapProviderStateConverter( private fun SwapQuoteUM.Error.convertToErrorContent(): SwapProviderState { val additionalBadge = when { - needApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList + isNeedApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList else -> AdditionalBadge.Empty } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt index 8d2c61caf1..a9312fc84a 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt @@ -66,8 +66,8 @@ internal class SwapChooseTokenNetworkModel @Inject constructor( } private fun initContent() { - val userWallet = getUserWalletUseCase(params.userWalletId).getOrElse { - Timber.e("Failed to get user wallet: $it") + val userWallet = getUserWalletUseCase(params.userWalletId).getOrElse { error -> + Timber.e("Failed to get user wallet: $error") swapChooseTokenAlertFactory.getGenericErrorState(params.onDismiss) return } @@ -87,8 +87,8 @@ internal class SwapChooseTokenNetworkModel @Inject constructor( cryptoCurrencyList = cryptoCurrencyList + params.initialCurrency, filterProviderTypes = SEND_WITH_SWAP_PROVIDER_TYPES, swapTxType = SwapTxType.SendWithSwap, - ).getOrElse { - Timber.e(it.toString()) + ).getOrElse { error -> + Timber.e(error.toString()) uiState.update( SwapChooseErrorStateTransformer( tokenName = params.token.name, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt index d5360e2add..0994844d9b 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt @@ -91,7 +91,8 @@ internal class SwapAlertFactory @Inject constructor( destinationAddress = confirmData?.enteredDestination.orEmpty(), tokenSymbol = confirmData?.toCryptoCurrencyStatus?.currency?.symbol.orEmpty(), amount = confirmData?.enteredAmount?.toString().orEmpty(), - fee = confirmData?.fee?.amount?.value?.toString().orEmpty(), + fee = confirmData?.fee?.amount?.value?.toString() + .orEmpty(), ), ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt index b105bed8a1..cf27f09d02 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt @@ -15,7 +15,7 @@ internal sealed class ConfirmUM { data class Content( override val isPrimaryButtonEnabled: Boolean = false, val isTransactionInProcess: Boolean, - val showTapHelp: Boolean, + val isShowTapHelp: Boolean, val sendingFooter: TextReference, val notifications: ImmutableList, val tosUM: TosUM?, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt index 2006967f4f..300dd949eb 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt @@ -109,10 +109,17 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( activeComponent.updateState(model.uiState.value.destinationUM) } is SendWithSwapConfirmComponent -> { + val fromCurrency = params.currency + val fromDerivationIndex = model.accountFlow.value?.derivationIndex?.value + .takeIf { model.isAccountModeFlow.value } analyticsEventHandler.send( CommonSendAnalyticEvents.ConfirmationScreenOpened( categoryName = model.analyticCategoryName, source = model.analyticsSendSource, + sendBlockchain = fromCurrency.network.name, + sendToken = fromCurrency.symbol, + fromDerivationIndex = fromDerivationIndex, + toDerivationIndex = null, ), ) if (model.currentRoute.value.isEditMode) { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt index ab38425164..e7e6ac5e7d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt @@ -13,7 +13,7 @@ import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents internal sealed class SendWithSwapAnalyticEvents( event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(category = CommonSendAnalyticEvents.SEND_CATEGORY, event = event, params = params) { data class TransactionScreenOpened( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index c0d119b50f..3fcd6d870c 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -94,11 +94,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( field = MutableStateFlow(params.sendWithSwapUM) val primaryCurrencyStatus: CryptoCurrencyStatus = params.primaryCryptoCurrencyStatusFlow.value - val secondaryCurrencyStatus: CryptoCurrencyStatus? = amountUM?.secondaryCryptoCurrencyStatus val primaryFeePaidCurrencyStatus: CryptoCurrencyStatus = params.primaryFeePaidCurrencyStatusFlow.value - val secondaryCurrency: CryptoCurrency = requireNotNull(amountUM?.secondaryCryptoCurrencyStatus?.currency) { - "Crypto currency must not be null" - } private val swapTransactionSender = swapTransactionSenderFactory.create(params.userWallet) @@ -111,6 +107,11 @@ internal class SendWithSwapConfirmModel @Inject constructor( private val feeSelectorUM get() = uiState.value.feeSelectorUM as? FeeSelectorUM.Content + val secondaryCurrencyStatus: CryptoCurrencyStatus? = amountUM?.secondaryCryptoCurrencyStatus + val secondaryCurrency: CryptoCurrency = requireNotNull(amountUM?.secondaryCryptoCurrencyStatus?.currency) { + "Crypto currency must not be null" + } + val confirmData: ConfirmData get() { val amountUM = amountUM @@ -270,13 +271,13 @@ internal class SendWithSwapConfirmModel @Inject constructor( uiState.transformerUpdate(SendWithSwapConfirmSendingStateTransformer(false)) swapAlertFactory.getSendTransactionErrorState( error = error, - onFailedTxEmailClick = { + onFailedTxEmailClick = { _ -> modelScope.launch { swapAlertFactory.onFailedTxEmailClick( userWallet = params.userWallet, cryptoCurrency = confirmData.fromCryptoCurrencyStatus?.currency, confirmData = confirmData, - errorMessage = error.toString(), + errorMessage = error?.toString().orEmpty(), ) } }, @@ -320,8 +321,8 @@ internal class SendWithSwapConfirmModel @Inject constructor( modelScope.launch { val isShowTapHelp = isSendTapHelpEnabledUseCase.invokeSync().getOrElse { false } if (confirmUM is ConfirmUM.Empty) { - uiState.update { - it.copy( + uiState.update { state -> + state.copy( confirmUM = SendWithSwapConfirmInitialStateTransformer( isShowTapHelp = isShowTapHelp, ).transform(uiState.value.confirmUM), @@ -335,9 +336,9 @@ internal class SendWithSwapConfirmModel @Inject constructor( private fun subscribeOnTapHelpUpdates() { isSendTapHelpEnabledUseCase().getOrNull() ?.onEach { showTapHelp -> - uiState.update { - val confirmUM = it.confirmUM as? ConfirmUM.Content - it.copy(confirmUM = confirmUM?.copy(showTapHelp = showTapHelp) ?: it.confirmUM) + uiState.update { state -> + val confirmUM = state.confirmUM as? ConfirmUM.Content + state.copy(confirmUM = confirmUM?.copy(isShowTapHelp = showTapHelp) ?: state.confirmUM) } }?.launchIn(modelScope) } @@ -376,12 +377,12 @@ internal class SendWithSwapConfirmModel @Inject constructor( flow2 = swapNotificationsUpdateListener.hasErrorFlow, ) { hasSendError, hasSwapError -> val hasError = hasSendError || hasSwapError - uiState.update { - val feeUM = it.feeSelectorUM as? FeeSelectorUM.Content - it.copy( - confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( + uiState.update { state -> + val feeUM = state.feeSelectorUM as? FeeSelectorUM.Content + state.copy( + confirmUM = (state.confirmUM as? ConfirmUM.Content)?.copy( isPrimaryButtonEnabled = !hasError && feeUM != null, - ) ?: it.confirmUM, + ) ?: state.confirmUM, ) } }.launchIn(modelScope) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt index 3602881267..1c6a439eeb 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt @@ -98,8 +98,8 @@ internal class SwapTransactionSender @AssistedInject constructor( toAddress = destination, expressProvider = provider, rateType = rateType, - expressOperationType, - ).getOrElse { onExpressError(it); return } + expressOperationType = expressOperationType, + ).getOrElse { error -> onExpressError(error); return } createAndSendCexTransaction( fromAmount = fromAmount, @@ -141,9 +141,9 @@ internal class SwapTransactionSender @AssistedInject constructor( destination = swapTransaction.txTo, userWalletId = userWallet.walletId, network = fromStatus.currency.network, - ).getOrElse { - Timber.e(it, "Failed to create swap CEX tx data") - onSendError(SendTransactionError.UnknownError(Exception(it))) + ).getOrElse { error -> + Timber.e(error, "Failed to create swap CEX tx data") + onSendError(SendTransactionError.UnknownError(Exception(error))) return } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt index 64e34d1673..1beaabe312 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt @@ -12,7 +12,7 @@ internal class SendWithSwapConfirmInitialStateTransformer( return ConfirmUM.Content( isPrimaryButtonEnabled = false, isTransactionInProcess = false, - showTapHelp = isShowTapHelp, + isShowTapHelp = isShowTapHelp, sendingFooter = TextReference.EMPTY, notifications = persistentListOf(), tosUM = null, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt index 3176e70a6c..53fd2df373 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt @@ -109,16 +109,16 @@ internal class SendWithSwapConfirmationNotificationsTransformer : Transformer ConfirmUM.Content.LegalUM( title = resourceReference(R.string.common_terms_of_use), - link = it, + link = termsOfUse, ) }, - policyLink = expressProvider.privacyPolicy?.let { + policyLink = expressProvider.privacyPolicy?.let { privacyPolicy -> ConfirmUM.Content.LegalUM( title = resourceReference(R.string.common_privacy_policy), - link = it, + link = privacyPolicy, ) }, ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt index 3d6968a554..629d37898d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt @@ -132,8 +132,8 @@ internal class SendWithSwapModel @Inject constructor( } override fun resetSendWithSwapNavigation(resetNavigation: Boolean) { - uiState.update { - it.copy( + uiState.update { state -> + state.copy( destinationUM = DestinationUM.Empty(), feeSelectorUM = FeeSelectorUM.Loading, confirmUM = ConfirmUM.Empty, @@ -166,8 +166,8 @@ internal class SendWithSwapModel @Inject constructor( userWallet = wallet getPrimaryCurrencyStatusUpdates(params.currency) }, - ifLeft = { - Timber.w(it.toString()) + ifLeft = { error -> + Timber.w(error.toString()) swapAlertFactory.getGenericErrorState( expressError = ExpressError.UnknownError, onFailedTxEmailClick = { @@ -175,7 +175,7 @@ internal class SendWithSwapModel @Inject constructor( swapAlertFactory.onFailedTxEmailClick( userWallet = userWallet, cryptoCurrency = params.currency, - errorMessage = it.toString(), + errorMessage = error.toString(), ) } }, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt index 0ccf5aada7..c106e29968 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt @@ -344,7 +344,7 @@ private fun SendWithSwapSuccessContent_Preview() { txUrl = "https://tangem.com", provider = ExpressProvider( providerId = "changelly", - rateTypes = listOf(), + rateTypes = emptyList(), name = "Changelly", type = ExpressProviderType.CEX, imageLarge = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/changelly-1024.png", diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt index fe0e00831b..9e3064dafc 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt @@ -36,7 +36,7 @@ internal fun SendWithSwapContent( stackState: ChildStack, onLinkClick: (String) -> Unit, ) { - val navigationUM = navigationUM as? NavigationUM.Content ?: return + val navigationUMContent = navigationUM as? NavigationUM.Content ?: return Column( modifier = Modifier @@ -47,9 +47,9 @@ internal fun SendWithSwapContent( horizontalAlignment = Alignment.CenterHorizontally, ) { AppBarWithBackButton( - text = navigationUM.title.resolveReference(), - onBackClick = navigationUM.backIconClick, - iconRes = navigationUM.additionalIconRes, + text = navigationUMContent.title.resolveReference(), + onBackClick = navigationUMContent.backIconClick, + iconRes = navigationUMContent.additionalIconRes, modifier = Modifier.height(TangemTheme.dimens.size56), ) Children( @@ -93,7 +93,7 @@ internal fun SendWithSwapContent( ) } NavigationPrimaryButton( - navigationUM.primaryButton, + navigationUMContent.primaryButton, modifier = Modifier.padding( start = 16.dp, end = 16.dp, diff --git a/features/swap/impl/detekt-baseline-debug.xml b/features/swap/impl/detekt-baseline-debug.xml index 44b4ff0d46..72441faba2 100644 --- a/features/swap/impl/detekt-baseline-debug.xml +++ b/features/swap/impl/detekt-baseline-debug.xml @@ -55,10 +55,6 @@ NoNameShadowing:SwapModel.kt$SwapModel${ it.cryptoCurrencyStatus } NoNameShadowing:SwapModel.kt$SwapModel${ it.cryptoCurrencyStatus.currency.id.value == id } NoNameShadowing:SwapModel.kt$SwapModel${ it.key == selectedSwapProvider } - NonBooleanPropertyPrefixedWithIs:StateBuilder.kt$StateBuilder$private val isAccountsModeProvider: Provider<Boolean> - NonBooleanPropertyPrefixedWithIs:StateBuilder.kt$StateBuilder$private val isBalanceHiddenProvider: Provider<Boolean> - NonBooleanPropertyPrefixedWithIs:SwapModel.kt$SwapModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:TokensDataConverter.kt$TokensDataConverter$private val isBalanceHiddenProvider: Provider<Boolean> NullableToStringCall:SwapModel.kt$SwapModel$${currencyStatus.value.amount} NullableToStringCall:SwapModel.kt$SwapModel$${it.value.amount} NullableToStringCall:TransactionCard.kt$data.toString() diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 8d8ed4d072..c1ee5ff99d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -23,7 +23,7 @@ sealed class SwapEvents( params = mapOf("Token" to token), ) - data object SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked") + class SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked") data class ChooseTokenScreenOpened(val availableTokens: Boolean) : SwapEvents( event = "Choose Token Screen Opened", @@ -43,7 +43,7 @@ sealed class SwapEvents( params = mapOf("Send Token" to sendToken, "Receive Token" to receiveToken), ) - data object ButtonGivePermissionClicked : SwapEvents(event = "Button - Give permission") + class ButtonGivePermissionClicked : SwapEvents(event = "Button - Give permission") data class ButtonPermissionApproveClicked( val sendToken: String, @@ -58,10 +58,11 @@ sealed class SwapEvents( ), ) - data object ButtonPermissionCancelClicked : SwapEvents(event = "Button - Permission Cancel") + class ButtonPermissionCancelClicked : SwapEvents(event = "Button - Permission Cancel") - data object ButtonSwipeClicked : SwapEvents(event = "Button - Swipe") + class ButtonSwipeClicked : SwapEvents(event = "Button - Swipe") + @Suppress("NullableToStringCall") data class SwapInProgressScreen( val provider: SwapProvider, val commission: FeeType, // Market / Fast @@ -69,6 +70,8 @@ sealed class SwapEvents( val receiveBlockchain: String, val sendToken: String, val receiveToken: String, + val fromDerivationIndex: Int?, + val toDerivationIndex: Int?, ) : SwapEvents( event = "Swap in Progress Screen Opened", params = mapOf( @@ -78,10 +81,11 @@ sealed class SwapEvents( "Receive Token" to receiveToken, "Send Blockchain" to sendBlockchain, "Receive Blockchain" to receiveBlockchain, + "Account Derivation From or To (optional)" to "$fromDerivationIndex, $toDerivationIndex", ), ) - data object ProviderClicked : SwapEvents("Provider Clicked") + class ProviderClicked : SwapEvents("Provider Clicked") data class ProviderChosen(val provider: SwapProvider) : SwapEvents( event = "Provider Chosen", @@ -98,7 +102,7 @@ sealed class SwapEvents( params = mapOf("Token" to token), ) - data object NoticeNoAvailableTokensToSwap : SwapEvents("Notice - No Available Tokens To Swap") + class NoticeNoAvailableTokensToSwap : SwapEvents("Notice - No Available Tokens To Swap") data class NoticeNotEnoughFee(val token: String, val blockchain: String) : SwapEvents( event = "Notice - Not Enough Fee", diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index d227b21dfd..d5884d3b72 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -375,7 +375,7 @@ internal class SwapModel @Inject constructor( ) { // exceptional case if (selectedCurrency == null) { - analyticsEventHandler.send(SwapEvents.NoticeNoAvailableTokensToSwap) + analyticsEventHandler.send(SwapEvents.NoticeNoAvailableTokensToSwap()) uiState = stateBuilder.createNoAvailableTokensToSwapState( uiStateHolder = uiState, fromToken = initialFromStatus, @@ -847,6 +847,8 @@ internal class SwapModel @Inject constructor( val fee = dataState.selectedFee?.feeType ?: return val fromCurrency = dataState.fromCryptoCurrency?.currency ?: return val toCurrency = dataState.toCryptoCurrency?.currency ?: return + val fromDerivationIndex = dataState.fromAccount?.derivationIndex?.value + val toDerivationIndex = dataState.toAccount?.derivationIndex?.value analyticsEventHandler.send( SwapEvents.SwapInProgressScreen( @@ -856,6 +858,8 @@ internal class SwapModel @Inject constructor( receiveBlockchain = toCurrency.network.name, sendToken = fromCurrency.symbol, receiveToken = toCurrency.symbol, + fromDerivationIndex = fromDerivationIndex, + toDerivationIndex = toDerivationIndex, ), ) } @@ -1271,7 +1275,7 @@ internal class SwapModel @Inject constructor( private fun onAmountSelected(selected: Boolean) { if (selected) { - analyticsEventHandler.send(SwapEvents.SendTokenBalanceClicked) + analyticsEventHandler.send(SwapEvents.SendTokenBalanceClicked()) } } @@ -1312,7 +1316,7 @@ internal class SwapModel @Inject constructor( }, onChangeCardsClicked = { onChangeCardsClicked() - analyticsEventHandler.send(SwapEvents.ButtonSwipeClicked) + analyticsEventHandler.send(SwapEvents.ButtonSwipeClicked()) }, onBackClicked = { val bottomSheet = uiState.bottomSheetConfig @@ -1331,10 +1335,10 @@ internal class SwapModel @Inject constructor( onReduceByAmount = ::onReduceAmountClicked, openPermissionBottomSheet = { singleTaskScheduler.cancelTask() - analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked) + analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked()) uiState = stateBuilder.showPermissionBottomSheet(uiState) { startLoadingQuotesFromLastState(isSilent = true) - analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked) + analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked()) uiState = stateBuilder.dismissBottomSheet(uiState) } }, @@ -1362,7 +1366,7 @@ internal class SwapModel @Inject constructor( } }, onProviderClick = { providerId -> - analyticsEventHandler.send(SwapEvents.ProviderClicked) + analyticsEventHandler.send(SwapEvents.ProviderClicked()) val states = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates() val pricesLowerBest = getPricesLowerBest(providerId, states) uiState = stateBuilder.showSelectProviderBottomSheet( diff --git a/features/tangempay/details/impl/detekt-baseline-debug.xml b/features/tangempay/details/impl/detekt-baseline-debug.xml index 8e67fe2685..68aa7140b9 100644 --- a/features/tangempay/details/impl/detekt-baseline-debug.xml +++ b/features/tangempay/details/impl/detekt-baseline-debug.xml @@ -16,7 +16,6 @@ MultilineLambdaItParameter:TangemPayDetailsScreen.kt${ TangemDropdownItem( item = it.dropdownItem, dismissParent = { showDropdownMenu = false }, ) } MultilineLambdaItParameter:TangemPayTxHistoryUiManager.kt$TangemPayTxHistoryUiManager${ it.status !is PaginationStatus.None && it.status !is PaginationStatus.InitialLoading && it.status !is PaginationStatus.InitialLoadingError } NullCheckOnMutableProperty:GoogleWalletUtil.kt$GoogleWalletUtil$if (walletIntent != null) { walletIntent } else { try { context.packageManager.getLaunchIntentForPackage(WALLET_PACKAGE_NAME) ?.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } .also { walletIntent = it } } catch (exception: Exception) { Timber.tag(TAG).e(exception) null } } - NullableBooleanCheck:TangemPayDetailsModel.kt$TangemPayDetailsModel$cardDetailsRepository.isAddToWalletDone().getOrNull() ?: false ReusedModifierInstance:DefaultTangemPayDetailsContainerComponent.kt$DefaultTangemPayDetailsContainerComponent$Content(modifier = modifier) ReusedModifierInstance:TangemPayChangePinCodeSuccessScreen.kt$Column( modifier .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { SuccessContent( modifier = Modifier .fillMaxWidth() .weight(1f), ) PrimaryButton( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp) .padding(bottom = 16.dp) .navigationBarsPadding(), text = stringResourceSafe(R.string.common_done), onClick = onClick, ) } ReusedModifierInstance:TangemPayChangePinScreen.kt$Column( modifier = modifier .fillMaxWidth() .padding(top = 48.dp) .padding(horizontal = 36.dp) .weight(1f), horizontalAlignment = Alignment.CenterHorizontally, ) { Text( text = stringResourceSafe(R.string.visa_onboarding_pin_code_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, ) SpacerH16() Text( text = stringResourceSafe(R.string.visa_onboarding_pin_code_description), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, ) SpacerH(26.dp) PinCodeSection(state) } diff --git a/features/tangempay/onboarding/impl/detekt-baseline-debug.xml b/features/tangempay/onboarding/impl/detekt-baseline-debug.xml deleted file mode 100644 index 244ca0e5f5..0000000000 --- a/features/tangempay/onboarding/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - MultilineLambdaItParameter:TangemPayOnboardingModel.kt$TangemPayOnboardingModel${ TangemPayOnboardingScreenState.Content( onBack = it.onBack, onTermsClick = ::onTermsClick, buttonConfig = TangemPayOnboardingScreenState.Content.ButtonConfig( isLoading = false, onClick = ::onGetCardClick, ), ) } - MultilineLambdaItParameter:TangemPayOnboardingModel.kt$TangemPayOnboardingModel${ Timber.e("Error getCustomerInfo: ${it.errorCode}") uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) } - NullableToStringCall:TangemPayOnboardingModel.kt$TangemPayOnboardingModel$${result.leftOrNull()?.message} - - diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt index dc16c58424..5b9fed7d81 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt @@ -50,6 +50,7 @@ internal class TangemPayOnboardingModel @Inject constructor( field = MutableStateFlow(getInitialState()) init { + analytics.send(TangemPayAnalyticsEvents.ActivationScreenOpened()) modelScope.launch { when (params) { is TangemPayOnboardingComponent.Params.ContinueOnboarding -> { @@ -65,11 +66,9 @@ internal class TangemPayOnboardingModel @Inject constructor( } private fun showOnboarding() { - // TODO: move analytics to init block [REDACTED_JIRA] - analytics.send(TangemPayAnalyticsEvents.ActivationScreenOpened()) - uiState.update { + uiState.update { state -> TangemPayOnboardingScreenState.Content( - onBack = it.onBack, + onBack = state.onBack, onTermsClick = ::onTermsClick, buttonConfig = TangemPayOnboardingScreenState.Content.ButtonConfig( isLoading = false, @@ -123,6 +122,7 @@ internal class TangemPayOnboardingModel @Inject constructor( urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK) } + @Suppress("NullableToStringCall") private fun onGetCardClick() { analytics.send(TangemPayAnalyticsEvents.GetCardClicked()) uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true)) @@ -140,8 +140,8 @@ internal class TangemPayOnboardingModel @Inject constructor( repository.getCustomerInfo( userWalletId = userWalletId, ).fold( - ifLeft = { - Timber.e("Error getCustomerInfo: ${it.errorCode}") + ifLeft = { error -> + Timber.e("Error getCustomerInfo: ${error.errorCode}") uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) }, ifRight = { customerInfo -> diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 57fe70672c..ba40b749c4 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -59,6 +59,8 @@ dependencies { /** Feature Apis */ implementation(projects.features.tester.api) implementation(projects.features.pushNotifications.api) + implementation(projects.features.news.newsDetails.api) + implementation(projects.features.news.newsDetails.impl) /* SDK */ implementation(tangemDeps.blockchain) diff --git a/features/tester/impl/detekt-baseline-debug.xml b/features/tester/impl/detekt-baseline-debug.xml index 4a5ba6a9ce..be0960afae 100644 --- a/features/tester/impl/detekt-baseline-debug.xml +++ b/features/tester/impl/detekt-baseline-debug.xml @@ -9,7 +9,6 @@ ExplicitCollectionElementAccessMethod:TestPushAddKeyDataTransformer.kt$TestPushAddKeyDataTransformer$mutableData.set(index = index, updated) ExplicitCollectionElementAccessMethod:TestPushAddValueDataTransformer.kt$TestPushAddValueDataTransformer$mutableData.set(index = index, updated) MaxChainedCallsOnSameLine:BlockchainProvidersScreen.kt$ProvidersDnDTarget$event.toAndroidDragEvent().clipData.getItemAt(0).text.toString().toInt() - MultilineLambdaItParameter:ApiEnvironmentComparator.kt$ApiEnvironmentComparator${ when (it) { ApiEnvironment.DEV -> 0 ApiEnvironment.DEV_2 -> 1 ApiEnvironment.DEV_3 -> 2 ApiEnvironment.STAGE -> 3 ApiEnvironment.MOCK -> 4 ApiEnvironment.PROD -> 5 } } MultilineLambdaItParameter:BlockchainProvidersScreen.kt${ value = it state.onValueChange(it.text) } MultilineLambdaItParameter:BlockchainProvidersViewModel.kt$BlockchainProvidersViewModel${ if (it.blockchainId == blockchainId) { it.update() } else { it } } MultilineLambdaItParameter:BlockchainProvidersViewModel.kt$BlockchainProvidersViewModel${ it.copyProvidersUM(blockchainId = id) { copy( addPublicProviderDialog = addPublicProviderDialog.copy( hasError = !PatternsCompat.WEB_URL.matcher(url).matches(), ), ) } } @@ -23,12 +22,10 @@ MultilineLambdaItParameter:TesterAccountsViewModel.kt$TesterAccountsViewModel${ it.copy( accountListBottomSheetConfig = it.accountListBottomSheetConfig.copy( isAccountsShown = false, ), ) } MultilineLambdaItParameter:TesterAccountsViewModel.kt$TesterAccountsViewModel${ it.copy( walletSelector = it.walletSelector.copy(selected = userWallet), accountListBottomSheetConfig = it.accountListBottomSheetConfig.copy( accounts = getWalletAccounts(userWallet.walletId), ), ) } MultilineLambdaItParameter:TesterActionsScreen.kt${ builder.setStream( FileProvider.getUriForFile(activity, "${activity.packageName}.provider", it), ) } - MultilineLambdaItParameter:TesterActivity.kt$TesterActivity${ val route = when (it) { ButtonUM.FEATURE_TOGGLES -> TesterScreen.FEATURE_TOGGLES ButtonUM.EXCLUDED_BLOCKCHAINS -> TesterScreen.EXCLUDED_BLOCKCHAINS ButtonUM.ENVIRONMENT_TOGGLES -> TesterScreen.ENVIRONMENTS_TOGGLES ButtonUM.BLOCKCHAIN_PROVIDERS -> TesterScreen.BLOCKCHAIN_PROVIDERS ButtonUM.TESTER_ACTIONS -> TesterScreen.TESTER_ACTIONS ButtonUM.TEST_PUSHES -> TesterScreen.TEST_PUSHES ButtonUM.ACCOUNTS -> TesterScreen.ACCOUNTS } innerTesterRouter.open(route) } NoNameShadowing:AccountsScreen.kt$content NoNameShadowing:BlockchainProvidersViewModel.kt$BlockchainProvidersViewModel${ onAddProviderClick(id = blockchain.id, url = it) } NoNameShadowing:BlockchainProvidersViewModel.kt$BlockchainProvidersViewModel${ onPublicProviderUrlChange(id = blockchain.id, url = it) } NoNameShadowing:EnvironmentsTogglesViewModel.kt$EnvironmentsTogglesViewModel${ it.environment == currentEnvironment } - NoNameShadowing:TesterActivity.kt$TesterActivity${ val route = when (it) { ButtonUM.FEATURE_TOGGLES -> TesterScreen.FEATURE_TOGGLES ButtonUM.EXCLUDED_BLOCKCHAINS -> TesterScreen.EXCLUDED_BLOCKCHAINS ButtonUM.ENVIRONMENT_TOGGLES -> TesterScreen.ENVIRONMENTS_TOGGLES ButtonUM.BLOCKCHAIN_PROVIDERS -> TesterScreen.BLOCKCHAIN_PROVIDERS ButtonUM.TESTER_ACTIONS -> TesterScreen.TESTER_ACTIONS ButtonUM.TEST_PUSHES -> TesterScreen.TEST_PUSHES ButtonUM.ACCOUNTS -> TesterScreen.ACCOUNTS } innerTesterRouter.open(route) } PropertyUsedBeforeDeclaration:BlockchainProvidersViewModel.kt$BlockchainProvidersViewModel$_state PropertyUsedBeforeDeclaration:BlockchainProvidersViewModel.kt$BlockchainProvidersViewModel$searchQuery PropertyUsedBeforeDeclaration:EnvironmentsTogglesViewModel.kt$EnvironmentsTogglesViewModel$_uiState diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index bab7b098d3..393fac33ad 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -35,6 +35,11 @@ import com.tangem.feature.tester.presentation.providers.ui.BlockchainProvidersSc import com.tangem.feature.tester.presentation.providers.viewmodel.BlockchainProvidersViewModel import com.tangem.feature.tester.presentation.testpush.ui.TestPushScreen import com.tangem.feature.tester.presentation.testpush.viewmodel.TestPushViewModel +import com.tangem.feature.tester.presentation.news.ui.NewsScreen +import com.tangem.feature.tester.presentation.news.viewmodel.NewsViewModel +import com.tangem.features.news.details.impl.MockArticlesFactory +import com.tangem.features.news.details.impl.ui.NewsDetailsContent +import com.tangem.features.news.details.impl.ui.NewsDetailsUM import dagger.hilt.android.AndroidEntryPoint import kotlinx.collections.immutable.persistentSetOf import javax.inject.Inject @@ -82,9 +87,10 @@ internal class TesterActivity : ComposeActivity() { ButtonUM.TESTER_ACTIONS, ButtonUM.TEST_PUSHES, ButtonUM.ACCOUNTS, + ButtonUM.NEWS, ), - onButtonClick = { - val route = when (it) { + onButtonClick = { buttonUM -> + val route = when (buttonUM) { ButtonUM.FEATURE_TOGGLES -> TesterScreen.FEATURE_TOGGLES ButtonUM.EXCLUDED_BLOCKCHAINS -> TesterScreen.EXCLUDED_BLOCKCHAINS ButtonUM.ENVIRONMENT_TOGGLES -> TesterScreen.ENVIRONMENTS_TOGGLES @@ -92,6 +98,7 @@ internal class TesterActivity : ComposeActivity() { ButtonUM.TESTER_ACTIONS -> TesterScreen.TESTER_ACTIONS ButtonUM.TEST_PUSHES -> TesterScreen.TEST_PUSHES ButtonUM.ACCOUNTS -> TesterScreen.ACCOUNTS + ButtonUM.NEWS -> TesterScreen.NEWS } innerTesterRouter.open(route) @@ -162,6 +169,27 @@ internal class TesterActivity : ComposeActivity() { AccountsScreen(state) } + + composable(route = TesterScreen.NEWS.name) { + val viewModel = hiltViewModel().apply { + setupNavigation(innerTesterRouter) + } + val state by viewModel.uiState.collectAsStateWithLifecycle() + + NewsScreen(state) + } + + composable(route = TesterScreen.NEWS_DETAILS.name) { + NewsDetailsContent( + state = NewsDetailsUM( + articles = MockArticlesFactory.createMockArticles(), + selectedArticleIndex = 0, + onLikeClick = { }, + onShareClick = { }, + ), + onBackClick = { innerTesterRouter.back() }, + ) + } } } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/entity/AccountsUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/entity/AccountsUM.kt index 245c9a99f6..d196e87bfd 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/entity/AccountsUM.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/entity/AccountsUM.kt @@ -13,11 +13,40 @@ internal data class AccountsUM( ) { data class Button( - val title: String, + val id: ID, + val title: String = createTitle(id), val isInProgress: Boolean = false, val isEnabled: Boolean = true, - val onClick: (title: String) -> Unit, - ) + val onClick: () -> Unit, + ) { + + enum class ID { + ShowAccountList, + FetchAccounts, + FillOutList, + FillOutArchivedList, + ArchiveAll, + SortByDerivationIndex, + ClearETag, + } + + fun reset(): Button { + return copy(title = createTitle(id), isInProgress = false, isEnabled = true) + } + + companion object { + + private fun createTitle(id: ID): String = when (id) { + ID.ShowAccountList -> "Show the account list" + ID.FetchAccounts -> "Fetch accounts" + ID.FillOutList -> "Fill out the list (up to 20)" + ID.FillOutArchivedList -> "Fill out the archived list" + ID.ArchiveAll -> "Archive all" + ID.SortByDerivationIndex -> "Sort by derivation index" + ID.ClearETag -> "Clear ETag" + } + } + } data class WalletSelector( val selected: UserWallet?, diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/ui/AccountsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/ui/AccountsScreen.kt index 1f19494869..46f9cc8aba 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/ui/AccountsScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/ui/AccountsScreen.kt @@ -206,7 +206,7 @@ private fun LazyListScope.ManageAccountsButtons(buttons: ImmutableList PrimaryButton( text = button.title, - onClick = { button.onClick(button.title) }, + onClick = button.onClick, modifier = Modifier .padding(horizontal = 16.dp, vertical = 8.dp) .fillMaxWidth(), diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt index 41c3f707e8..0d855a3880 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt @@ -60,7 +60,7 @@ internal class TesterAccountsViewModel @Inject constructor( userWallets.firstOrNull()?.let(::onWalletSelect) - userWallets.mapIndexed { index, userWallet -> + userWallets.map { userWallet -> singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId = userWallet.walletId)) .distinctUntilChanged() .onEach { accountList -> @@ -102,12 +102,16 @@ internal class TesterAccountsViewModel @Inject constructor( }, ), buttons = persistentListOf( - AccountsUM.Button(title = "Show the account list") { updateAccountsList() }, - AccountsUM.Button(title = "Fetch accounts", onClick = ::fetchAccounts), - AccountsUM.Button(title = "Fill out the list (up to 20)", onClick = ::fillOutAccountList), - AccountsUM.Button(title = "Archive all", onClick = ::archiveAllAccounts), - AccountsUM.Button(title = "Sort by derivation index", onClick = ::sortAccountsByIndex), - AccountsUM.Button(title = "Clear ETag") { clearETag() }, + AccountsUM.Button(id = AccountsUM.Button.ID.ShowAccountList, onClick = ::updateAccountsList), + AccountsUM.Button(id = AccountsUM.Button.ID.FetchAccounts, onClick = ::fetchAccounts), + AccountsUM.Button(id = AccountsUM.Button.ID.FillOutList, onClick = ::onFillOutClick), + AccountsUM.Button( + id = AccountsUM.Button.ID.FillOutArchivedList, + onClick = ::fillOutArchivedAccountList, + ), + AccountsUM.Button(id = AccountsUM.Button.ID.ArchiveAll, onClick = ::onArchiveAllClick), + AccountsUM.Button(id = AccountsUM.Button.ID.SortByDerivationIndex, onClick = ::sortAccountsByIndex), + AccountsUM.Button(id = AccountsUM.Button.ID.ClearETag) { clearETag() }, ), ) } @@ -137,19 +141,15 @@ internal class TesterAccountsViewModel @Inject constructor( } } - private fun fetchAccounts(title: String) { - viewModelScope.launch { - val userWalletId = uiState.value.walletSelector.selected?.walletId ?: return@launch - - toggleButtonProgress(title = title, isInProgress = true) + private fun fetchAccounts() { + val userWalletId = uiState.value.walletSelector.selected?.walletId ?: return + withProgress(id = AccountsUM.Button.ID.FetchAccounts) { withContext(dispatchers.default) { singleAccountListFetcher( params = SingleAccountListFetcher.Params(userWalletId = userWalletId), ) } - - toggleButtonProgress(title = title, isInProgress = false) } } @@ -160,84 +160,113 @@ internal class TesterAccountsViewModel @Inject constructor( } } - private fun fillOutAccountList(title: String) { + private fun onFillOutClick() { val userWalletId = getUserWallet()?.walletId ?: return - viewModelScope.launch { - toggleButtonProgress(title = title, isInProgress = true) + withProgress(id = AccountsUM.Button.ID.FillOutList) { + fillOutAccountList(userWalletId = userWalletId) + } + } - withContext(dispatchers.default) { - var accountList = walletAccounts.value[userWalletId] ?: return@withContext - val occupiedIndexes = accountList.accounts - .filterIsInstance() - .map { it.derivationIndex.value } - .toSet() + private fun fillOutArchivedAccountList() { + val userWalletId = getUserWallet()?.walletId ?: return + val accountList = walletAccounts.value[userWalletId] ?: return + if (accountList.totalArchivedAccounts == AccountList.MAX_ARCHIVED_ACCOUNTS_COUNT) return - var nextIndex = 0 - @Suppress("LoopWithTooManyJumpStatements") // never mind for Tester Menu - while (accountList.canAddMoreAccounts) { - while (occupiedIndexes.contains(nextIndex)) { - nextIndex++ - } + val id = AccountsUM.Button.ID.FillOutArchivedList - val derivationIndex = DerivationIndex(nextIndex).getOrNull() ?: break + viewModelScope.launch(dispatchers.default) { + var currentTotalArchived = accountList.totalArchivedAccounts - val newAccount = Account.CryptoPortfolio.invoke( - accountId = AccountId.forCryptoPortfolio(userWalletId, derivationIndex), - name = "Account #$nextIndex", - icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - derivationIndex = nextIndex, - cryptoCurrencies = emptySet(), + while (currentTotalArchived < AccountList.MAX_ARCHIVED_ACCOUNTS_COUNT) { + updateButton(id) { button -> + button.copy( + title = "In progress... ($currentTotalArchived/${AccountList.MAX_ARCHIVED_ACCOUNTS_COUNT})", + isEnabled = false, ) - .getOrNull() - ?: break - - (accountList + newAccount) - .onRight { accountList = it } - .getOrNull() - ?: break - - nextIndex++ } - accountsCRUDRepository.saveAccounts(accountList) + fillOutAccountList(userWalletId = userWalletId) + archiveAll(userWalletId = userWalletId) + + accountsCRUDRepository.getAccountListSync(userWalletId).onSome { + currentTotalArchived = it.totalArchivedAccounts + } } - toggleButtonProgress(title = title, isInProgress = false) + updateButton(id = id, button = AccountsUM.Button::reset) } } - private fun archiveAllAccounts(title: String) { - val userWalletId = getUserWallet()?.walletId ?: return - val accountList = walletAccounts.value[userWalletId] ?: return + private suspend fun fillOutAccountList(userWalletId: UserWalletId) { + withContext(dispatchers.default) { + var accountList = walletAccounts.value[userWalletId] ?: return@withContext - viewModelScope.launch { - toggleButtonProgress(title = title, isInProgress = true) + var nextIndex = accountList.totalAccounts + @Suppress("LoopWithTooManyJumpStatements") // never mind for Tester Menu + while (accountList.canAddMoreAccounts) { + val derivationIndex = DerivationIndex(nextIndex).getOrNull() ?: break - withContext(dispatchers.default) { - val updatedAccountList = AccountList.invoke( - userWalletId = accountList.userWalletId, - accounts = listOf(accountList.mainAccount), - totalAccounts = accountList.totalAccounts, - sortType = accountList.sortType, - groupType = accountList.groupType, + val newAccount = Account.CryptoPortfolio.invoke( + accountId = AccountId.forCryptoPortfolio(userWalletId, derivationIndex), + name = "Account #$nextIndex", + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = nextIndex, + cryptoCurrencies = emptySet(), ) - .getOrElse { return@withContext } + .getOrNull() + ?: break - accountsCRUDRepository.saveAccounts(accountList = updatedAccountList) + (accountList + newAccount) + .onRight { accountList = it } + .getOrNull() + ?: break + + nextIndex++ } - toggleButtonProgress(title = title, isInProgress = false) + accountsCRUDRepository.saveAccounts(accountList) } } - private fun sortAccountsByIndex(title: String) { + private fun onArchiveAllClick() { + val userWalletId = getUserWallet()?.walletId ?: return + + withProgress(id = AccountsUM.Button.ID.ArchiveAll) { + archiveAll(userWalletId = userWalletId) + } + } + + private suspend fun archiveAll(userWalletId: UserWalletId) { + val accountList = walletAccounts.value[userWalletId] ?: return + val possibleToArchive = AccountList.MAX_ARCHIVED_ACCOUNTS_COUNT - accountList.totalArchivedAccounts + + if (possibleToArchive == 0) return + + withContext(dispatchers.default) { + val updatedAccountList = AccountList.invoke( + userWalletId = accountList.userWalletId, + accounts = if (possibleToArchive > AccountList.MAX_ACCOUNTS_COUNT - 1) { + listOf(accountList.mainAccount) + } else { + accountList.accounts.subList(fromIndex = 0, toIndex = accountList.accounts.size - possibleToArchive) + }, + totalAccounts = accountList.totalAccounts, + totalArchivedAccounts = accountList.totalArchivedAccounts, + sortType = accountList.sortType, + groupType = accountList.groupType, + ) + .getOrElse { return@withContext } + + accountsCRUDRepository.saveAccounts(accountList = updatedAccountList) + } + } + + private fun sortAccountsByIndex() { val userWalletId = getUserWallet()?.walletId ?: return val accountList = walletAccounts.value[userWalletId] ?: return - viewModelScope.launch { - toggleButtonProgress(title = title, isInProgress = true) - + withProgress(id = AccountsUM.Button.ID.SortByDerivationIndex) { withContext(dispatchers.default) { val sortedAccountList = AccountList.invoke( userWalletId = accountList.userWalletId, @@ -245,6 +274,7 @@ internal class TesterAccountsViewModel @Inject constructor( .filterIsInstance() .sortedBy { it.derivationIndex.value }, totalAccounts = accountList.totalAccounts, + totalArchivedAccounts = accountList.totalArchivedAccounts, sortType = accountList.sortType, groupType = accountList.groupType, ) @@ -252,8 +282,6 @@ internal class TesterAccountsViewModel @Inject constructor( accountsCRUDRepository.saveAccounts(accountList = sortedAccountList) } - - toggleButtonProgress(title = title, isInProgress = false) } } @@ -266,22 +294,30 @@ internal class TesterAccountsViewModel @Inject constructor( ?: persistentListOf() } - private fun toggleButtonProgress(title: String, isInProgress: Boolean) { - uiState.update { state -> - state.copy( - buttons = state.buttons.updateButton(title) { - it.copy(isInProgress = isInProgress) - }, - ) + private fun withProgress(id: AccountsUM.Button.ID, block: suspend () -> Unit) { + viewModelScope.launch { + toggleButtonProgress(id = id, isInProgress = true) + try { + block() + } finally { + toggleButtonProgress(id = id, isInProgress = false) + } } } - private fun ImmutableList.updateButton( - title: String, - button: (AccountsUM.Button) -> AccountsUM.Button, - ): ImmutableList { - return this - .map { if (it.title == title) button(it) else it } - .toImmutableList() + private fun toggleButtonProgress(id: AccountsUM.Button.ID, isInProgress: Boolean) { + updateButton(id) { + it.copy(isInProgress = isInProgress) + } + } + + private fun updateButton(id: AccountsUM.Button.ID, button: (AccountsUM.Button) -> AccountsUM.Button) { + uiState.update { state -> + state.copy( + buttons = state.buttons + .map { if (it.id == id) button(it) else it } + .toImmutableList(), + ) + } } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/utils/ApiEnvironmentComparator.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/utils/ApiEnvironmentComparator.kt index ac81103c06..69b0648f7d 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/utils/ApiEnvironmentComparator.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/utils/ApiEnvironmentComparator.kt @@ -10,14 +10,15 @@ import com.tangem.datasource.api.common.config.ApiEnvironmentConfig */ internal object ApiEnvironmentComparator : Comparator { - private val apiEnvironmentPriorityMap = ApiEnvironment.entries.associateWith { - when (it) { + private val apiEnvironmentPriorityMap = ApiEnvironment.entries.associateWith { environment -> + when (environment) { ApiEnvironment.DEV -> 0 ApiEnvironment.DEV_2 -> 1 ApiEnvironment.DEV_3 -> 2 ApiEnvironment.STAGE -> 3 - ApiEnvironment.MOCK -> 4 - ApiEnvironment.PROD -> 5 + ApiEnvironment.STAGE_2 -> 4 + ApiEnvironment.MOCK -> 5 + ApiEnvironment.PROD -> 6 } } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt index fa30058309..00a019e9c4 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt @@ -25,5 +25,6 @@ data class TesterMenuUM( TESTER_ACTIONS(R.string.tester_actions), TEST_PUSHES(R.string.test_push), ACCOUNTS(R.string.accounts), + NEWS(R.string.news), } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt index e87d942c19..854706cec2 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt @@ -14,4 +14,6 @@ internal enum class TesterScreen { BLOCKCHAIN_PROVIDERS, TEST_PUSHES, ACCOUNTS, + NEWS, + NEWS_DETAILS, } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/state/NewsUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/state/NewsUM.kt new file mode 100644 index 0000000000..b832b363d8 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/state/NewsUM.kt @@ -0,0 +1,16 @@ +package com.tangem.feature.tester.presentation.news.state + +import androidx.annotation.StringRes +import com.tangem.feature.tester.impl.R +import kotlinx.collections.immutable.ImmutableSet + +data class NewsUM( + val onBackClick: () -> Unit, + val buttons: ImmutableSet, + val onButtonClick: (ButtonUM) -> Unit, +) { + + enum class ButtonUM(@StringRes val textResId: Int) { + NEWS_DETAILS(R.string.news_details), + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/ui/NewsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/ui/NewsScreen.kt new file mode 100644 index 0000000000..2f0b9c690c --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/ui/NewsScreen.kt @@ -0,0 +1,62 @@ +package com.tangem.feature.tester.presentation.news.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.impl.R +import com.tangem.feature.tester.presentation.news.state.NewsUM +import kotlinx.collections.immutable.ImmutableSet + +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun NewsScreen(state: NewsUM, modifier: Modifier = Modifier) { + BackHandler(onBack = state.onBackClick) + + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary), + ) { + stickyHeader { AppBar(onBackClick = state.onBackClick) } + + NewsButtons(buttons = state.buttons, onButtonClick = state.onButtonClick) + } +} + +@Composable +private fun AppBar(onBackClick: () -> Unit) { + AppBarWithBackButton( + onBackClick = onBackClick, + text = stringResourceSafe(id = R.string.news), + containerColor = TangemTheme.colors.background.primary, + ) +} + +@Suppress("FunctionNaming") +private fun LazyListScope.NewsButtons( + buttons: ImmutableSet, + onButtonClick: (NewsUM.ButtonUM) -> Unit, +) { + items(buttons.toList()) { button -> + PrimaryButton( + text = stringResourceSafe(button.textResId), + onClick = { onButtonClick(button) }, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + ) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/viewmodel/NewsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/viewmodel/NewsViewModel.kt new file mode 100644 index 0000000000..5ed86201e4 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/viewmodel/NewsViewModel.kt @@ -0,0 +1,44 @@ +package com.tangem.feature.tester.presentation.news.viewmodel + +import androidx.lifecycle.ViewModel +import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter +import com.tangem.feature.tester.presentation.navigation.TesterScreen +import com.tangem.feature.tester.presentation.news.state.NewsUM +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@HiltViewModel +internal class NewsViewModel @Inject constructor() : ViewModel() { + + private val _uiState = MutableStateFlow(createInitialState()) + val uiState: StateFlow = _uiState + + private var router: InnerTesterRouter? = null + + fun setupNavigation(router: InnerTesterRouter) { + this.router = router + } + + private fun createInitialState(): NewsUM { + return NewsUM( + onBackClick = ::onBackClick, + buttons = persistentSetOf( + NewsUM.ButtonUM.NEWS_DETAILS, + ), + onButtonClick = ::onButtonClick, + ) + } + + private fun onBackClick() { + router?.back() + } + + private fun onButtonClick(button: NewsUM.ButtonUM) { + when (button) { + NewsUM.ButtonUM.NEWS_DETAILS -> router?.open(TesterScreen.NEWS_DETAILS) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index c47c184c25..15b31a07c6 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -18,4 +18,6 @@ Share logs Test push Accounts + News + News details diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt index decfd54c50..50bc8ae792 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt @@ -56,12 +56,12 @@ internal class TokenReceiveAssetsModel @Inject constructor( ), ) - private fun configureEnsStatus(): AnalyticsParam.EnsStatus { + private fun configureEnsStatus(): AnalyticsParam.EmptyFull { val hasEnsAddress = params.addresses.any { it.type == ReceiveAddress.Type.Ens } return if (hasEnsAddress) { - AnalyticsParam.EnsStatus.FULL + AnalyticsParam.EmptyFull.Full } else { - AnalyticsParam.EnsStatus.EMPTY + AnalyticsParam.EmptyFull.Empty } } } \ No newline at end of file diff --git a/features/tokendetails/impl/detekt-baseline-debug.xml b/features/tokendetails/impl/detekt-baseline-debug.xml index 2cd9989be6..71ab124fb2 100644 --- a/features/tokendetails/impl/detekt-baseline-debug.xml +++ b/features/tokendetails/impl/detekt-baseline-debug.xml @@ -24,20 +24,17 @@ MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ internalUiState.value = stateFactory.getStateWithUpdatedHidden( isBalanceHidden = it.isBalanceHidden, ) } MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ sendButtonsEvents(it.states) internalUiState.value = stateFactory.getManageButtonsState(actions = it.states) } MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ val updatedState = stateFactory.getStateWithNotifications(it) notificationsAnalyticsSender.send(internalUiState.value, updatedState.notifications) internalUiState.value = updatedState } - MultilineLambdaItParameter:TokenDetailsOnrampTransactionStateConverter.kt$TokenDetailsOnrampTransactionStateConverter${ analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) clickIntents.onGoToProviderClick(it) } MultilineLambdaItParameter:TokenDetailsScreen.kt${ Notification( modifier = itemModifier.animateItem(), config = it.config, iconTint = when (it) { is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent is TokenDetailsNotification.UsedOutdatedData -> TangemTheme.colors.text.attention else -> null }, ) } MultilineLambdaItParameter:TokenDetailsTopAppBar.kt${ TangemDropdownItem( item = it, dismissParent = { showDropdownMenu = false }, ) } MultilineLambdaItParameter:TokenStakingBlock.kt${ when (it) { is StakingBlockUM.TemporaryUnavailable -> StakingTemporaryUnavailableBlock() is StakingBlockUM.Loading -> StakingLoading() is StakingBlockUM.Staked -> StakingBalanceBlock( state = it, isBalanceHidden = isBalanceHidden, ) is StakingBlockUM.StakeAvailable -> StakingAvailableContent( state = it, ) } } NamedArguments:TokenDetailsLoadedBalanceConverter.kt$TokenDetailsLoadedBalanceConverter$formatFiatAmount( status.value, stakingFiatAmount, currentState.selectedBalanceType, appCurrencyProvider(), ) NamedArguments:TokenDetailsSwapTransactionsStateConverter.kt$TokenDetailsSwapTransactionsStateConverter$createStateInfo( transaction, toCryptoCurrency, fromCryptoCurrency, toFiatAmount, fromFiatAmount, ) NestedScopeFunctions:TokenDetailsBalanceSelectStateConverter.kt$TokenDetailsBalanceSelectStateConverter$let { cryptoCurrencyStatus.value.fiatRate?.multiply(it) } - NonBooleanPropertyPrefixedWithIs:TokenDetailsModel.kt$TokenDetailsModel$private val isDemoCardUseCase: IsDemoCardUseCase NullableBooleanCheck:TokenDetailsSwapTransactionsStateConverter.kt$TokenDetailsSwapTransactionsStateConverter$transaction.status?.hasLongTime ?: false NullableToStringCall:DefaultTokenDetailsDeepLinkHandler.kt$DefaultTokenDetailsDeepLinkHandler$$networkId NullableToStringCall:DefaultTokenDetailsDeepLinkHandler.kt$DefaultTokenDetailsDeepLinkHandler$$tokenId NullableToStringCall:TokenDetailsStakingInfoConverter.kt$TokenDetailsStakingInfoConverter$$stakingCryptoAmount NullableToStringCall:TokenDetailsStakingInfoConverter.kt$TokenDetailsStakingInfoConverter$$stakingEntryInfo - NullableToStringCall:TokenDetailsStakingInfoConverter.kt$TokenDetailsStakingInfoConverter$$yieldBalance PropertyUsedBeforeDeclaration:ExpressStatusBottomSheetStateProvider.kt$ExpressStatusBottomSheetStateProvider$network PropertyUsedBeforeDeclaration:ExpressStatusBottomSheetStateProvider.kt$ExpressStatusBottomSheetStateProvider$token PropertyUsedBeforeDeclaration:TokenDetailsModel.kt$TokenDetailsModel$uiState @@ -49,7 +46,6 @@ UseEmptyCounterpart:TokenDetailsAnalyticsEvent.kt$TokenDetailsAnalyticsEvent$mapOf() UseEmptyCounterpart:TokenDetailsAnalyticsEvent.kt$TokenDetailsAnalyticsEvent.Notice$mapOf() UseOrEmpty:ExchangeStatusFactory.kt$ExchangeStatusFactory$savedTransactions ?.flatMap { setOf(it.fromCryptoCurrency.id, it.toCryptoCurrency.id) } ?.toSet() ?.getQuotesOrEmpty() ?: emptySet() - UseOrEmpty:TokenDetailsStakingInfoConverter.kt$TokenDetailsStakingInfoConverter$yieldBalance?.balance?.items ?: emptyList() VarCouldBeVal:TokenDetailsModel.kt$TokenDetailsModel$private var expressTxStatusTaskScheduler = SingleTaskScheduler<PersistentList<ExpressTransactionStateUM>>() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 16ce0aecda..0b39af391f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -535,6 +535,7 @@ internal class TokenDetailsModel @Inject constructor( token = cryptoCurrency.symbol, blockchain = cryptoCurrency.network.name, status = unavailabilityReason.toReasonAnalyticsText(), + derivationIndex = getAccountIndexOrNull(), ), ) @@ -560,6 +561,7 @@ internal class TokenDetailsModel @Inject constructor( token = cryptoCurrency.symbol, blockchain = cryptoCurrency.network.name, status = null, + derivationIndex = getAccountIndexOrNull(), ), ) router.openTokenDetails(userWalletId = userWalletId, currency = cryptoCurrency) @@ -581,6 +583,7 @@ internal class TokenDetailsModel @Inject constructor( token = cryptoCurrency.symbol, blockchain = cryptoCurrency.network.name, status = unavailabilityReason.toReasonAnalyticsText(), + derivationIndex = getAccountIndexOrNull(), ), ) @@ -701,6 +704,7 @@ internal class TokenDetailsModel @Inject constructor( token = cryptoCurrency.symbol, blockchain = cryptoCurrency.network.name, status = unavailabilityReason.toReasonAnalyticsText(), + derivationIndex = getAccountIndexOrNull(), ), ) @@ -769,6 +773,12 @@ internal class TokenDetailsModel @Inject constructor( showErrorIfDemoModeOrElse(action = ::openExplorer) } + private fun getAccountIndexOrNull(): Int? { + val account = account + val isNotMainAccount = account != null && !account.isMainAccount + return if (isNotMainAccount) account.derivationIndex.value else null + } + private fun openExplorer() { val currencyStatus = cryptoCurrencyStatus ?: return @@ -881,7 +891,7 @@ internal class TokenDetailsModel @Inject constructor( PromoAnalyticsEvent.PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Token, program = PromoAnalyticsEvent.Program.Empty, // Use it on new promo action - action = PromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed, + action = PromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed(), ), ) } @@ -894,7 +904,7 @@ internal class TokenDetailsModel @Inject constructor( PromoAnalyticsEvent.PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Token, program = PromoAnalyticsEvent.Program.Empty, // Use it on new promo action - action = PromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked, + action = PromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked(), ), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt index 95139be280..0f9a0741ac 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt @@ -5,7 +5,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance @@ -29,8 +29,8 @@ internal class TokenDetailsBalanceSelectStateConverter( return this } - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data - val stakingCryptoAmount = yieldBalance?.getTotalWithRewardsStakingBalance( + val stakingBalance = cryptoCurrencyStatus.value.stakingBalance as? StakingBalance.Data + val stakingCryptoAmount = stakingBalance?.getTotalWithRewardsStakingBalance( cryptoCurrencyStatus.currency.network.rawId, ) val stakingFiatAmount = stakingCryptoAmount?.let { cryptoCurrencyStatus.value.fiatRate?.multiply(it) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 306315e766..ee290cda1f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -9,7 +9,7 @@ import com.tangem.core.ui.format.bigdecimal.* import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents @@ -73,7 +73,7 @@ internal class TokenDetailsLoadedBalanceConverter( status: CryptoCurrencyStatus, ): TokenDetailsBalanceBlockState { val stakingCryptoAmount = - (status.value.yieldBalance as? YieldBalance.Data)?.getTotalWithRewardsStakingBalance( + (status.value.stakingBalance as? StakingBalance.Data)?.getTotalWithRewardsStakingBalance( status.currency.network.rawId, ) val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt index ec37185065..742e94dab2 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt @@ -83,9 +83,9 @@ internal class TokenDetailsOnrampTransactionStateConverter( fallbackResId = R.drawable.ic_currency_24, ), iconState = getIconState(value.status), - onGoToProviderClick = { - analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) - clickIntents.onGoToProviderClick(it) + onGoToProviderClick = { url -> + analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider()) + clickIntents.onGoToProviderClick(url) }, onClick = { clickIntents.onExpressTransactionClick(value.txId) }, onDisposeExpressStatus = clickIntents::onConfirmDisposeExpressStatus, @@ -117,7 +117,7 @@ internal class TokenDetailsOnrampTransactionStateConverter( private fun onProviderClick(externalTxUrl: String?) = if (externalTxUrl != null) { { - analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) + analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider()) clickIntents.onGoToProviderClick(externalTxUrl) } } else { @@ -269,7 +269,7 @@ internal class TokenDetailsOnrampTransactionStateConverter( icon = R.drawable.ic_arrow_top_right_24, text = resourceReference(R.string.common_go_to_provider), onClick = { - analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) + analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider()) clickIntents.onGoToProviderClick(externalTxUrl) }, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt index b16d554602..e87c73a48e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt @@ -10,7 +10,7 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.RewardBlockType -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.utils.getRewardStakingBalance @@ -54,18 +54,29 @@ internal class TokenDetailsStakingInfoConverter( } } + @Suppress("CyclomaticComplexMethod") private fun getStakingInfoBlock(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlockUM? { - val yieldBalance = status.value.yieldBalance as? YieldBalance.Data + val stakingBalance = status.value.stakingBalance as? StakingBalance.Data - val stakingCryptoAmount = yieldBalance?.getTotalStakingBalance(status.currency.network.rawId) - val pendingBalances = yieldBalance?.balance?.items ?: emptyList() + val stakingCryptoAmount = stakingBalance?.getTotalStakingBalance(status.currency.network.rawId) + + val hasPendingBalances = when (stakingBalance) { + is StakingBalance.Data.StakeKit -> stakingBalance.balance.items.isNotEmpty() + is StakingBalance.Data.P2P -> !stakingBalance.unstakingAmount.isNullOrZero() + null -> false + } + val pendingAmount = when (stakingBalance) { + is StakingBalance.Data.StakeKit -> stakingBalance.balance.items.sumOf { it.amount } + is StakingBalance.Data.P2P -> stakingBalance.unstakingAmount + null -> BigDecimal.ZERO + } val iconState = state.tokenInfoBlockState.iconState Timber.i( """ getStakingInfoBlock: - – yieldBalance: $yieldBalance + – stakingBalance: ${stakingBalance ?: "null"} – stakingCryptoAmount: $stakingCryptoAmount – stakingEntryInfo: $stakingEntryInfo """.trimIndent(), @@ -73,16 +84,24 @@ internal class TokenDetailsStakingInfoConverter( return when { stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null -> { - if (pendingBalances.isEmpty()) { + if (!hasPendingBalances) { getStakeAvailableState(stakingEntryInfo, iconState, isStakingButtonEnabled(status)) } else { - getStakedBlockWithFiatAmount(status, pendingBalances.sumOf { it.amount }, null) + getStakedBlockWithFiatAmount(status, pendingAmount, null) } } stakingCryptoAmount.isNullOrZero() && stakingEntryInfo == null -> { null } - else -> getStakedBlockWithFiatAmount(status, stakingCryptoAmount, yieldBalance?.getRewardStakingBalance()) + else -> getStakedBlockWithFiatAmount( + status = status, + stakingAmount = stakingCryptoAmount, + rewardAmount = when (stakingBalance) { + is StakingBalance.Data.StakeKit -> stakingBalance.getRewardStakingBalance() + is StakingBalance.Data.P2P -> stakingBalance.totalRewards + else -> BigDecimal.ZERO + }, + ) } } diff --git a/features/txhistory/impl/detekt-baseline-debug.xml b/features/txhistory/impl/detekt-baseline-debug.xml deleted file mode 100644 index 7a98042e54..0000000000 --- a/features/txhistory/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - BooleanPropertyNaming:TxHistoryListManager.kt$TxHistoryListManager$val clearUiBatches = state.status is PaginationStatus.InitialLoading && batchListState.status is PaginationStatus.Paginating - MultilineLambdaItParameter:TxHistoryUiManager.kt$TxHistoryUiManager${ it.status !is PaginationStatus.None && it.status !is PaginationStatus.InitialLoading && it.status !is PaginationStatus.InitialLoadingError } - UseEmptyCounterpart:TxHistoryListState.kt$TxHistoryListState$listOf() - - diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListManager.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListManager.kt index 1c8153f1cb..485c064e0a 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListManager.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListManager.kt @@ -88,13 +88,13 @@ internal class TxHistoryListManager( private fun updateState(batchListState: BatchListState>) { state.update { state -> - val clearUiBatches = + val shouldClearUiBatches = state.status is PaginationStatus.InitialLoading && batchListState.status is PaginationStatus.Paginating state.copy( status = batchListState.status, uiBatches = uiManager.createOrUpdateUiBatches( newCurrencyBatches = batchListState.data, - clearUiBatches = clearUiBatches, + shouldClearUiBatches = shouldClearUiBatches, ), ) } diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListState.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListState.kt index 8e553fe6ac..e27adfb4ce 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListState.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryListState.kt @@ -6,5 +6,5 @@ import com.tangem.pagination.PaginationStatus data class TxHistoryListState( val status: PaginationStatus<*> = PaginationStatus.None, - val uiBatches: List>> = listOf(), + val uiBatches: List>> = emptyList(), ) \ No newline at end of file diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryUiManager.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryUiManager.kt index 3bf17cde84..84a2b5be73 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryUiManager.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryUiManager.kt @@ -22,10 +22,10 @@ internal class TxHistoryUiManager( @OptIn(ExperimentalCoroutinesApi::class) val items: Flow> = state // filter initial states, since we dont emit loading items as UI items - .filter { - it.status !is PaginationStatus.None && - it.status !is PaginationStatus.InitialLoading && - it.status !is PaginationStatus.InitialLoadingError + .filter { state -> + state.status !is PaginationStatus.None && + state.status !is PaginationStatus.InitialLoading && + state.status !is PaginationStatus.InitialLoadingError } .mapLatest { state -> state.uiBatches.asSequence() @@ -36,10 +36,10 @@ internal class TxHistoryUiManager( fun createOrUpdateUiBatches( newCurrencyBatches: List>>, - clearUiBatches: Boolean, + shouldClearUiBatches: Boolean, ): List>> { val currentUiBatches = state.value.uiBatches - val batches = if (clearUiBatches) mutableListOf() else currentUiBatches.toMutableList() + val batches = if (shouldClearUiBatches) mutableListOf() else currentUiBatches.toMutableList() for ((key, data) in newCurrencyBatches) { // Find if batch with same key exists diff --git a/features/wallet-settings/impl/detekt-baseline-debug.xml b/features/wallet-settings/impl/detekt-baseline-debug.xml index aba860e5ef..ecf2e0cce8 100644 --- a/features/wallet-settings/impl/detekt-baseline-debug.xml +++ b/features/wallet-settings/impl/detekt-baseline-debug.xml @@ -1,9 +1,5 @@ - - NonBooleanPropertyPrefixedWithIs:AccountItemsDelegate.kt$AccountItemsDelegate$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:WalletSettingsModel.kt$WalletSettingsModel$private val isDemoCardUseCase: IsDemoCardUseCase - NonBooleanPropertyPrefixedWithIs:WalletSettingsModel.kt$WalletSettingsModel$private val isUpgradeWalletNotificationEnabledUseCase: IsUpgradeWalletNotificationEnabledUseCase - + diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 826d89ac75..b5ba400298 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -25,6 +25,8 @@ import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.account.supplier.SingleAccountListSupplier +import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.PortfolioId @@ -88,6 +90,8 @@ internal class WalletSettingsModel @Inject constructor( private val dismissUpgradeWalletNotificationUseCase: DismissUpgradeWalletNotificationUseCase, private val unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase, private val accountsFeatureToggles: AccountsFeatureToggles, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val singleAccountListSupplier: SingleAccountListSupplier, private val accountListSortingSaver: AccountListSortingSaver, ) : Model() { @@ -115,7 +119,18 @@ internal class WalletSettingsModel @Inject constructor( init { getUserWalletUseCase.invoke(params.userWalletId).onRight { wallet -> trackingContextProxy.addContext(wallet) - analyticsEventHandler.send(WalletSettingsAnalyticEvents.WalletSettingsScreenOpened) + modelScope.launch { + val accountsCount = if (isAccountsModeEnabledUseCase.invokeSync()) { + singleAccountListSupplier(wallet.walletId) + .first() + .accounts + .size + } else { + null + } + val event = WalletSettingsAnalyticEvents.WalletSettingsScreenOpened(accountsCount) + analyticsEventHandler.send(event) + } } fun combineUI(wallet: UserWallet) = combine( @@ -220,7 +235,7 @@ internal class WalletSettingsModel @Inject constructor( }, onReferralClick = { onReferralClick(userWallet) }, onManageTokensClick = { - analyticsEventHandler.send(Settings.ButtonManageTokens) + analyticsEventHandler.send(Settings.ButtonManageTokens()) router.push( AppRoute.ManageTokens( source = Source.SETTINGS, @@ -384,7 +399,6 @@ internal class WalletSettingsModel @Inject constructor( } private fun onUpgradeWalletClick() { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonHardwareUpdate) if (!state.value.isWalletBackedUp) { showMakeBackupAtFirstAlertBS( isUpgradeFlow = true, @@ -404,7 +418,7 @@ internal class WalletSettingsModel @Inject constructor( } private fun onBackupClick() { - analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonBackup) + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonBackup()) router.push(AppRoute.WalletBackup(params.userWalletId)) } @@ -438,10 +452,10 @@ internal class WalletSettingsModel @Inject constructor( AppRoute.CreateWalletBackup( userWalletId = params.userWalletId, isUpgradeFlow = isUpgradeFlow, - setAccessCode = true, + shouldSetAccessCode = true, analyticsSource = AnalyticsParam.ScreensSources.WalletSettings.value, analyticsAction = if (isUpgradeFlow) { - RecoveryPhraseScreenAction.Backup.value + RecoveryPhraseScreenAction.Upgrade.value } else { RecoveryPhraseScreenAction.AccessCode.value }, diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt index c9458a072e..0d40442830 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt @@ -2,6 +2,7 @@ package com.tangem.feature.walletsettings.utils import com.tangem.common.routing.AppRoute import com.tangem.common.ui.account.AccountPortfolioItemUMConverter +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router @@ -24,6 +25,7 @@ import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.domain.wallets.extension.isAccountsSupported import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM @@ -46,6 +48,7 @@ internal class AccountItemsDelegate @Inject constructor( private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val accountListSortingSaver: AccountListSortingSaver, private val accountsFeatureToggles: AccountsFeatureToggles, + private val analyticsEventHandler: AnalyticsEventHandler, ) { private val userWalletId = paramsContainer.require().userWalletId @@ -72,7 +75,10 @@ internal class AccountItemsDelegate @Inject constructor( ): List = buildList { fun AccountStatus.CryptoPortfolio.mapCryptoPortfolio(): WalletSettingsAccountsUM { val accountItemUM = AccountPortfolioItemUMConverter( - onClick = { openAccountDetails(this.account) }, + onClick = { + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonOpenExistingAccount()) + openAccountDetails(this.account) + }, appCurrency = appCurrency, accountBalance = this.tokenList.totalFiatBalance, isBalanceHidden = isBalanceHidden, @@ -107,14 +113,22 @@ internal class AccountItemsDelegate @Inject constructor( title = resourceReference(R.string.account_form_title_create), isAddAccountEnabled = isAddAccountEnabled, onAddAccountClick = { - if (isAddAccountEnabled) openAddAccount(userWalletId) else canNotAddAccountDialog() + if (isAddAccountEnabled) { + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonAddAccount()) + openAddAccount(userWalletId) + } else { + canNotAddAccountDialog() + } }, ), archivedAccounts = if (isArchivedAccountsEnabled) { BlockUM( text = resourceReference(R.string.account_archived_accounts), iconRes = R.drawable.ic_archive_24, - onClick = { openArchivedAccounts(userWalletId) }, + onClick = { + analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonArchivedAccounts()) + openArchivedAccounts(userWalletId) + }, ) } else { null diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountListSortingSaver.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountListSortingSaver.kt index 1f86f48958..52d5a6dbfc 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountListSortingSaver.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountListSortingSaver.kt @@ -1,7 +1,9 @@ package com.tangem.feature.walletsettings.utils +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.account.usecase.ApplyAccountListSortingUseCase import com.tangem.domain.models.account.AccountId +import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview @@ -24,6 +26,7 @@ import kotlin.time.Duration.Companion.seconds @OptIn(FlowPreview::class) internal class AccountListSortingSaver @Inject constructor( private val applyAccountListSortingUseCase: ApplyAccountListSortingUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, dispatchers: CoroutineDispatcherProvider, ) { @@ -41,6 +44,9 @@ internal class AccountListSortingSaver @Inject constructor( applyAccountListSortingUseCase.invoke(accountIds).onLeft { Timber.e("Error while saving account list sorting: $it") } + .onRight { + analyticsEventHandler.send(WalletSettingsAnalyticEvents.LongtapAccountsOrder()) + } } .launchIn(coroutineScope) } diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index ca7ea45cc6..81e2a852c5 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -124,6 +124,7 @@ dependencies { implementation(projects.features.tokenRecieve.api) implementation(projects.features.yieldSupply.api) implementation(projects.features.tangempay.details.api) + implementation(projects.features.feed.api) /** Common modules */ implementation(projects.common) diff --git a/features/wallet/impl/detekt-baseline-debug.xml b/features/wallet/impl/detekt-baseline-debug.xml index f13bc25643..f3a1111f2a 100644 --- a/features/wallet/impl/detekt-baseline-debug.xml +++ b/features/wallet/impl/detekt-baseline-debug.xml @@ -19,7 +19,6 @@ BooleanPropertyNaming:WalletManageButton.kt$WalletManageButton$/** Whether to dim content */ abstract val dimContent: Boolean BooleanPropertyNaming:WalletManageButton.kt$WalletManageButton.Swap$val showBadge: Boolean = false BooleanPropertyNaming:WalletModel.kt$WalletModel$private var needToRefreshWallet = false - BooleanPropertyNaming:WalletModel.kt$WalletModel$val initialDataProduced = tangemPayOnboardingRepository.isTangemPayInitialDataProduced() BooleanPropertyNaming:WalletNameMigrationUseCase.kt$WalletNameMigrationUseCase$private val useNewListRepository: Boolean BooleanPropertyNaming:WalletScreen.kt$val portfolioContent = state is WalletState.MultiCurrency.Content && state.tokensListState is WalletTokensListState.ContentState.PortfolioContent BooleanPropertyNaming:WalletScreen.kt$val showMarketsHint by remember { derivedStateOf { // Show hint only when there are items in the list // and when there a no items to scroll listState.layoutInfo.totalItemsCount > 0 && !listState.canScrollBackward && !listState.canScrollForward || listState.canScrollBackward && !listState.canScrollForward } } @@ -46,7 +45,6 @@ MultilineLambdaItParameter:SetRefreshStateTransformer.kt$SetRefreshStateTransformer${ it.mapNotNull { button -> when (button) { is WalletManageButton.Buy -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Receive -> button is WalletManageButton.Stake -> null is WalletManageButton.Swap -> null } } } MultilineLambdaItParameter:SetVisaInfoTransformer.kt$SetVisaInfoTransformer${ if (it is RefreshTokenExpiredException) { return getRefreshTokenExpiredState(prevState) } return prevState.copy( buttons = createVisaButtonsDimmed(), walletCardState = getErrorWalletCardState(prevState.walletCardState), balancesAndLimitBlockState = BalancesAndLimitsBlockState.Error, ) } MultilineLambdaItParameter:SingleWalletExpressStatusesSubscriber.kt$SingleWalletExpressStatusesSubscriber${ Timber.e("Unable to get primary currency status: $it") return@onEach } - MultilineLambdaItParameter:SingleWalletOnrampTransactionConverter.kt$SingleWalletOnrampTransactionConverter${ analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) clickIntents.onGoToProviderClick(it) } MultilineLambdaItParameter:TokenListAnalyticsSender.kt$TokenListAnalyticsSender${ val status = it.value if (status is CryptoCurrencyStatus.Loaded) { sendTokenBalancesForSpecificBlockchains(it, status) } } MultilineLambdaItParameter:TokenListStateConverter.kt$TokenListStateConverter${ if (isExtend) { clickIntents.onAccountCollapseClick(it) } else { clickIntents.onAccountExpandClick(it) } } MultilineLambdaItParameter:TxHistorySubscriber.kt$TxHistorySubscriber${ SetTxHistoryCountErrorTransformer( userWallet = userWallet, error = it, pendingTransactions = status.value.pendingTransactions, clickIntents = clickIntents, ) } @@ -72,7 +70,6 @@ MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) shareManager.shareText(text = it) } MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ onAddressTypeSelected( userWalletId = userWalletId, currency = currency, addressModel = it, ) } MultilineLambdaItParameter:WalletLoaderStorage.kt$WalletLoaderStorage${ it.forEach(Job::cancel) loaders.remove(id) } - MultilineLambdaItParameter:WalletModel.kt$WalletModel${ it .conflate() .distinctUntilChanged() .onEach { selectedWallet -> if (selectedWallet.isMultiCurrency) { selectedWalletAnalyticsSender.send(selectedWallet) } subscribeOnExpressTransactionsUpdates(selectedWallet) observeAndClearNFTCacheIfNeedUseCase(selectedWallet) } .flowOn(dispatchers.main) .launchIn(modelScope) } MultilineLambdaItParameter:WalletModel.kt$WalletModel${ walletScreenContentLoader.load( userWallet = it, clickIntents = clickIntents, coroutineScope = modelScope, isRefresh = true, ) } MultilineLambdaItParameter:WalletModel.kt$WalletModel${ walletsUpdateActionResolver.resolve( wallets = it, currentState = stateHolder.value, ) } MultilineLambdaItParameter:WalletNFTListSubscriber.kt$WalletNFTListSubscriber${ stateHolder.update( SetNFTCollectionsTransformer( userWalletId = userWallet.walletId, nftCollections = it, onItemClick = { clickIntents.onNFTClick(userWallet) }, ), ) } @@ -85,9 +82,7 @@ MultilineLambdaItParameter:WalletScreen.kt${ nftCollections( modifier = itemModifier, state = it.nftState, ) } MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ Timber.e( """ Unable to get user wallet |- ID: $userWalletId |- Exception: $it """.trimIndent(), ) null } MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ router.openOnboardingScreen( scanResponse = it.scanResponse, continueBackup = true, ) } - MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ when (it) { UnlockWalletError.AlreadyUnlocked -> Unit UnlockWalletError.ScannedCardWalletNotMatched -> { uiMessageSender.send( message = DialogMessage( title = resourceReference(R.string.common_warning), message = resourceReference(R.string.error_wrong_wallet_tapped), ), ) } UnlockWalletError.UnableToUnlock -> { Timber.e("Unable to unlock wallet with id: $selectedUserWalletId") uiMessageSender.send( SnackbarMessage(TextReference.Res(R.string.generic_error)), ) } UnlockWalletError.UserCancelled -> Unit UnlockWalletError.UserWalletNotFound -> { // This should never happen in this flow Timber.e("User wallet not found for unlock: $selectedUserWalletId") uiMessageSender.send( SnackbarMessage(TextReference.Res(R.string.generic_error)), ) } } } MultilineLambdaItParameter:WalletWithFundsChecker.kt$WalletWithFundsChecker${ val amount = it.value.amount ?: return@any false !amount.isZero() } - MultilineLambdaItParameter:WalletsUpdateActionResolver.kt$WalletsUpdateActionResolver${ if (it.warnings.any { it is WalletNotification.FinishWalletActivation }) { it.walletCardState.id } else { null } } NamedArguments:BasicAccountListSubscriber.kt$BasicAccountListSubscriber$updateContent(convertParams, appCurrency, yieldSupplyApyMap, stakingApyMap) NamedArguments:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents) NamedArguments:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents) @@ -111,23 +106,6 @@ NoNameShadowing:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ it is TokensListItemUM.Token } NoNameShadowing:WalletNFTItem.kt$modifier NoNameShadowing:WalletScreen.kt${ it.organizeTokensButtonConfig?.let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } - NoNameShadowing:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ when (it) { UnlockWalletError.AlreadyUnlocked -> Unit UnlockWalletError.ScannedCardWalletNotMatched -> { uiMessageSender.send( message = DialogMessage( title = resourceReference(R.string.common_warning), message = resourceReference(R.string.error_wrong_wallet_tapped), ), ) } UnlockWalletError.UnableToUnlock -> { Timber.e("Unable to unlock wallet with id: $selectedUserWalletId") uiMessageSender.send( SnackbarMessage(TextReference.Res(R.string.generic_error)), ) } UnlockWalletError.UserCancelled -> Unit UnlockWalletError.UserWalletNotFound -> { // This should never happen in this flow Timber.e("User wallet not found for unlock: $selectedUserWalletId") uiMessageSender.send( SnackbarMessage(TextReference.Res(R.string.generic_error)), ) } } } - NoNameShadowing:WalletsUpdateActionResolver.kt$WalletsUpdateActionResolver${ it is WalletNotification.FinishWalletActivation } - NonBooleanPropertyPrefixedWithIs:AccountDependencies.kt$AccountDependencies$val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:ExpandedAccountsHolder.kt$ExpandedAccountsHolder$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$private val isDemoCardUseCase: IsDemoCardUseCase - NonBooleanPropertyPrefixedWithIs:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$private val isNeedToBackupUseCase: IsNeedToBackupUseCase - NonBooleanPropertyPrefixedWithIs:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase - NonBooleanPropertyPrefixedWithIs:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory$private val isDemoCardUseCase: IsDemoCardUseCase - NonBooleanPropertyPrefixedWithIs:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory$private val isNeedToBackupUseCase: IsNeedToBackupUseCase - NonBooleanPropertyPrefixedWithIs:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory$private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase - NonBooleanPropertyPrefixedWithIs:OrganizeTokensModel.kt$OrganizeTokensModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:ScreenLifecycleProvider.kt$ScreenLifecycleProvider$val isBackgroundState: StateFlow<Boolean> = _isBackgroundState - NonBooleanPropertyPrefixedWithIs:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor$private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase - NonBooleanPropertyPrefixedWithIs:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor$private val isDemoCardUseCase: IsDemoCardUseCase - NonBooleanPropertyPrefixedWithIs:WalletModel.kt$WalletModel$private val isWalletsScrollPreviewEnabled: IsWalletsScrollPreviewEnabled - NonBooleanPropertyPrefixedWithIs:WalletScreen.kt$val isAutoScroll = remember { mutableStateOf(value = false) } - NonBooleanPropertyPrefixedWithIs:WalletScreen.kt$val isNavBarVisible = remember { mutableStateOf(true) } NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$$bitcoinCurrency NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$$bitcoinStatus NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$${cryptoCurrencies?.size} diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt index 97057fd80e..fb310ec8be 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt @@ -95,7 +95,7 @@ internal class OrganizeTokensModel @Inject constructor( val onBack = MutableSharedFlow() init { - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened) + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened()) getBalanceHidingSettingsUseCase() .onEach { @@ -117,7 +117,7 @@ internal class OrganizeTokensModel @Inject constructor( val list = cachedAccountStatusList ?: return if (list.sortType == TokensSortType.BALANCE) return - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance) + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance()) modelScope.launch { toggleTokenListSortingUseCaseV2(list).fold( @@ -132,7 +132,7 @@ internal class OrganizeTokensModel @Inject constructor( val list = cachedTokenList ?: return if (list.sortedBy == TokensSortType.BALANCE) return - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance) + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance()) modelScope.launch { toggleTokenListSortingUseCase(list).fold( @@ -150,7 +150,7 @@ internal class OrganizeTokensModel @Inject constructor( if (accountsFeatureToggles.isFeatureEnabled) { val list = cachedAccountStatusList ?: return - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group) + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group()) modelScope.launch { toggleTokenListGroupingUseCaseV2(list).fold( @@ -164,7 +164,7 @@ internal class OrganizeTokensModel @Inject constructor( } else { val list = cachedTokenList ?: return - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group) + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group()) modelScope.launch { toggleTokenListGroupingUseCase(list).fold( @@ -228,7 +228,7 @@ internal class OrganizeTokensModel @Inject constructor( } override fun onCancelClick() { - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Cancel) + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Cancel()) modelScope.launch { onBack.emit(Unit) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index acd97c7552..32454e1a3f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -1,17 +1,20 @@ package com.tangem.feature.wallet.child.wallet -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss +import com.arkivanov.essenty.lifecycle.doOnResume import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.decompose.ComposableDialogComponent @@ -22,6 +25,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogCon import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen import com.tangem.feature.walletsettings.component.RenameWalletComponent import com.tangem.features.biometry.AskBiometryComponent +import com.tangem.features.feed.entry.components.FeedEntryComponent +import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent import com.tangem.features.pushnotifications.api.PushNotificationsParams @@ -36,18 +41,28 @@ import kotlinx.coroutines.launch internal class WalletComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted navigate: (WalletRoute) -> Unit, + marketsEntryComponentFactory: MarketsEntryComponent.Factory, + feedEntryComponentFactory: FeedEntryComponent.Factory, private val renameWalletComponentFactory: RenameWalletComponent.Factory, - private val marketsEntryComponentFactory: MarketsEntryComponent.Factory, private val askBiometryComponentFactory: AskBiometryComponent.Factory, private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory, private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, private val yieldSupplyDepositedWarningComponent: YieldSupplyDepositedWarningComponent.Factory, + private val feedFeatureToggle: FeedFeatureToggle, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: WalletModel = getOrCreateModel() + private val feedEntryComponent by lazy { + feedEntryComponentFactory.create(child("feedEntryComponent")) + } + private val marketsEntryComponent by lazy { + marketsEntryComponentFactory.create(child("marketsEntryComponent")) + } + init { lifecycle.subscribe(model.screenLifecycleProvider) + doOnResume { model.onResume() } componentScope.launch { model.innerWalletRouter.navigateToFlow.collect { navigate(it) } } } @@ -112,15 +127,23 @@ internal class WalletComponent @AssistedInject constructor( }, ) - private val marketsEntryComponent = marketsEntryComponentFactory.create(child("marketsEntryComponent")) - @Composable override fun Content(modifier: Modifier) { + val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) } + var headerSize by remember { mutableStateOf(0.dp) } val dialog by dialog.subscribeAsState() WalletScreen( state = model.uiState.collectAsStateWithLifecycle().value, - marketsEntryComponent = marketsEntryComponent, + bottomSheetContent = { + BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = { headerSize = it }, + modifier = modifier, + ) + }, + bottomSheetHeaderHeightProvider = { headerSize }, + onBottomSheetStateChange = { bottomSheetState.value = it }, ) when (val dialog = dialog.child?.instance) { @@ -130,6 +153,27 @@ internal class WalletComponent @AssistedInject constructor( } } + @Composable + private fun BottomSheetContent( + bottomSheetState: State, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier = Modifier, + ) { + if (feedFeatureToggle.isFeedEnabled) { + feedEntryComponent.BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = onHeaderSizeChange, + modifier = modifier, + ) + } else { + marketsEntryComponent.BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = onHeaderSizeChange, + modifier = modifier, + ) + } + } + @AssistedFactory interface Factory { fun create(appComponentContext: AppComponentContext, navigate: (WalletRoute) -> Unit): WalletComponent diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 2338f56503..291743c337 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -7,9 +7,12 @@ import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent +import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.account.supplier.SingleAccountListSupplier +import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet @@ -40,6 +43,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.features.biometry.AskBiometryComponent +import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks import com.tangem.features.tangempay.TangemPayFeatureToggles @@ -96,6 +100,10 @@ internal class WalletModel @Inject constructor( private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, private val accountsFeatureToggles: AccountsFeatureToggles, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, + private val trackingContextProxy: TrackingContextProxy, + private val singleAccountListSupplier: SingleAccountListSupplier, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val feedFeatureToggle: FeedFeatureToggle, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -113,15 +121,11 @@ internal class WalletModel @Inject constructor( private var expressTxStatusTaskScheduler = SingleTaskScheduler() init { - analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened) - - screenLifecycleProvider.isBackgroundState - .onEach { isBackground -> - if (isBackground.not()) { - suggestToEnableBiometrics() - } - }.launchIn(modelScope) + if (!hotWalletFeatureToggles.isHotWalletEnabled) { + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpenedLegacy()) + } + updateMarketToggle() suggestToOpenMarkets() maybeMigrateNames() @@ -138,6 +142,18 @@ internal class WalletModel @Inject constructor( clickIntents.initialize(innerWalletRouter, modelScope) } + fun onResume() { + modelScope.launch(dispatchers.main) { + suggestToEnableBiometrics() + } + } + + private fun updateMarketToggle() { + stateHolder.update { + it.copy(isNewMarketEnabled = feedFeatureToggle.isFeedEnabled) + } + } + private fun updateYieldSupplyApy() { if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled) { modelScope.launch(dispatchers.default) { @@ -193,7 +209,6 @@ internal class WalletModel @Inject constructor( private suspend fun shouldShowAskBiometryBottomSheet(): Boolean { return if (hotWalletFeatureToggles.isHotWalletEnabled) { userWalletsListRepository.userWalletsSync().any { it is UserWallet.Cold } && - innerWalletRouter.isWalletLastScreen() && shouldShowAskBiometryUseCase() && canUseBiometryUseCase() } else { @@ -271,11 +286,34 @@ internal class WalletModel @Inject constructor( // It's okay here because we need to be able to observe the selected wallet changes @Suppress("DEPRECATION") private fun subscribeOnSelectedWalletFlow() { - getSelectedWalletUseCase().onRight { - it + getSelectedWalletUseCase().onRight { walletFlow -> + walletFlow .conflate() .distinctUntilChanged() .onEach { selectedWallet -> + trackingContextProxy.setContext(selectedWallet) + + if (hotWalletFeatureToggles.isHotWalletEnabled) { + modelScope.launch { + val hasMobileWallet = userWalletsListRepository.userWalletsSync() + .any { it is UserWallet.Hot } + val accountsCount = if (isAccountsModeEnabledUseCase.invokeSync()) { + singleAccountListSupplier(selectedWallet.walletId) + .first() + .accounts + .size + } else { + null + } + analyticsEventsHandler.send( + WalletScreenAnalyticsEvent.MainScreen.ScreenOpened( + hasMobileWallet = hasMobileWallet, + accountsCount = accountsCount, + ), + ) + } + } + if (selectedWallet.isMultiCurrency) { selectedWalletAnalyticsSender.send(selectedWallet) } @@ -416,6 +454,7 @@ internal class WalletModel @Inject constructor( when (action) { is WalletsUpdateActionResolver.Action.InitializeWallets -> initializeWallets(action) is WalletsUpdateActionResolver.Action.ReinitializeWallet -> reinitializeWallet(action) + is WalletsUpdateActionResolver.Action.ReinitializeWallets -> reinitializeWallets(action) is WalletsUpdateActionResolver.Action.AddWallet -> addWallet(action) is WalletsUpdateActionResolver.Action.DeleteWallet -> deleteWallet(action) is WalletsUpdateActionResolver.Action.UnlockWallet -> unlockWallet(action) @@ -520,6 +559,32 @@ internal class WalletModel @Inject constructor( ) } + private fun reinitializeWallets(action: WalletsUpdateActionResolver.Action.ReinitializeWallets) { + action.wallets.forEach { userWallet -> + walletScreenContentLoader.cancel(userWallet.walletId) + tokenListStore.remove(userWallet.walletId) + + walletScreenContentLoader.load( + userWallet = userWallet, + clickIntents = clickIntents, + coroutineScope = modelScope, + ) + + modelScope.launch(dispatchers.main) { + fetchWalletContent(userWallet = userWallet) + } + + stateHolder.update( + ReinitializeWalletTransformer( + prevWalletId = userWallet.walletId, + newUserWallet = userWallet, + clickIntents = clickIntents, + walletImageResolver = walletImageResolver, + ), + ) + } + } + private fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) { if (accountsFeatureToggles.isFeatureEnabled) { fetchWalletContent(userWallet = action.selectedWallet) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt index 6cce8840ea..82846a7624 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt @@ -64,8 +64,11 @@ internal class WalletsUpdateActionResolver @Inject constructor( selectedWallet: UserWallet, ): Action { return when { - isHotWalletUpgraded(state, wallets) -> { - getHotWalletsUpgradedAction(state, wallets) + isAnyHotWalletUpgraded(state, wallets) -> { + getHotWalletsUpgradedAction(state, wallets, selectedWallet) + } + isAnyHotWalletBackedUpChange(state, wallets) -> { + getHotWalletsBackedUpAction(state, wallets) } isWalletsCountChanged(state, wallets) -> { getChangeWalletsListAction(state, wallets, selectedWallet) @@ -79,31 +82,35 @@ internal class WalletsUpdateActionResolver @Inject constructor( isAnyWalletNameChanged(state, wallets) -> { getRenameWalletsAction(state, wallets) } - isAnyHotWalletBackedUpChange(state, wallets) -> { - getHotWalletsBackedUpAction(state, wallets) + isAnyWalletUnlocked(state, wallets) -> { + Action.UnlockWallet( + selectedWallet = selectedWallet, + unlockedWallets = wallets.filterNot(UserWallet::isLocked), + ) } - else -> getUpdateSelectedWalletAction(state, wallets, selectedWallet) + isSelectedWalletCardsCountChanged(state, selectedWallet) -> { + Action.UpdateWalletCardCount(selectedWallet) + } + else -> Action.Unknown } } private fun isAnyHotWalletBackedUpChange(state: WalletScreenState, wallets: List): Boolean { val incompleteActivationWalletIds = state.incompleteActivationWalletIds() - val walletsToUpdate = wallets.filter { - it is UserWallet.Hot && it.backedUp == incompleteActivationWalletIds.contains(it.walletId) + return wallets.any { + it is UserWallet.Hot && it.backedUp && incompleteActivationWalletIds.contains(it.walletId) } - return walletsToUpdate.isNotEmpty() } - private fun isHotWalletUpgraded(state: WalletScreenState, wallets: List): Boolean { - val previousWallet = state - .wallets - .getOrNull(state.selectedWalletIndex) - return when (previousWallet) { - is WalletState.MultiCurrency -> { - val wallet = wallets.firstOrNull { it.walletId == previousWallet.walletCardState.id } - previousWallet.type == WalletState.MultiCurrency.WalletType.Hot && wallet is UserWallet.Cold + private fun isAnyHotWalletUpgraded(state: WalletScreenState, wallets: List): Boolean { + return state.wallets.any { walletState -> + when (walletState) { + is WalletState.MultiCurrency -> { + val wallet = wallets.firstOrNull { it.walletId == walletState.walletCardState.id } + walletState.type == WalletState.MultiCurrency.WalletType.Hot && wallet is UserWallet.Cold + } + else -> false } - else -> false } } @@ -189,9 +196,15 @@ internal class WalletsUpdateActionResolver @Inject constructor( private fun getHotWalletsUpgradedAction( state: WalletScreenState, wallets: List, - ): Action.ReloadWallets { - val walletsToUpdate = wallets.filter { it.walletId == state.getPrevSelectedWallet().id } - return Action.ReloadWallets(walletsToUpdate) + selectedWallet: UserWallet, + ): Action.ReinitializeWallets { + val walletsToUpdate = wallets.filter { wallet -> + val previousState = state.wallets.firstOrNull { it.walletCardState.id == wallet.walletId } + ?: return@filter false + wallet is UserWallet.Cold && previousState is WalletState.MultiCurrency && + previousState.type == WalletState.MultiCurrency.WalletType.Hot + } + return Action.ReinitializeWallets(selectedWallet, walletsToUpdate) } private fun getRenameWalletsAction(state: WalletScreenState, wallets: List): Action.RenameWallets { @@ -205,29 +218,14 @@ internal class WalletsUpdateActionResolver @Inject constructor( ) } - private fun getUpdateSelectedWalletAction( - state: WalletScreenState, - wallets: List, - selectedWallet: UserWallet, - ): Action { - return when { - isSelectedWalletUnlocked(state, selectedWallet) -> { - Action.UnlockWallet( - selectedWallet = selectedWallet, - unlockedWallets = wallets.filterNot(UserWallet::isLocked), - ) - } - isSelectedWalletCardsCountChanged(state, selectedWallet) -> { - Action.UpdateWalletCardCount(selectedWallet) - } - else -> Action.Unknown + private fun isAnyWalletUnlocked(state: WalletScreenState, wallets: List): Boolean { + return state.wallets.any { walletState -> + val wallet = wallets.firstOrNull { it.walletId == walletState.walletCardState.id } ?: return@any false + !wallet.isLocked && + (walletState is WalletState.MultiCurrency.Locked || walletState is WalletState.SingleCurrency.Locked) } } - private fun isSelectedWalletUnlocked(state: WalletScreenState, selectedWallet: UserWallet): Boolean { - return state.isSelectedWalletLocked() && !selectedWallet.isLocked - } - private fun isSelectedWalletCardsCountChanged(state: WalletScreenState, selectedWallet: UserWallet): Boolean { if (selectedWallet !is UserWallet.Cold) return false val prevSelectedWallet = state.getPrevSelectedWallet() @@ -235,12 +233,6 @@ internal class WalletsUpdateActionResolver @Inject constructor( prevSelectedWallet.cardCount != selectedWallet.getCardsCount() } - private fun WalletScreenState.isSelectedWalletLocked(): Boolean { - val selectedWalletState = wallets.getOrNull(selectedWalletIndex) ?: error("Selected wallet is not found") - return selectedWalletState is WalletState.MultiCurrency.Locked || - selectedWalletState is WalletState.SingleCurrency.Locked - } - private fun WalletScreenState.getPrevSelectedWallet(): WalletCardState { return wallets .map(WalletState::walletCardState) @@ -249,9 +241,12 @@ internal class WalletsUpdateActionResolver @Inject constructor( } private fun WalletScreenState.incompleteActivationWalletIds(): List { - return wallets.mapNotNull { - if (it.warnings.any { it is WalletNotification.FinishWalletActivation }) { - it.walletCardState.id + return wallets.mapNotNull { wallet -> + if (wallet.warnings.any { it is WalletNotification.FinishWalletActivation } || + wallet.walletCardState is WalletState.MultiCurrency && + wallet.walletCardState.additionalInfo?.isHotBackedUp == false + ) { + wallet.walletCardState.id } else { null } @@ -306,6 +301,19 @@ internal class WalletsUpdateActionResolver @Inject constructor( } } + /** + * Reinitialize wallets + */ + data class ReinitializeWallets( + val selectedWallet: UserWallet, + val wallets: List, + ) : Action() { + + override fun toString(): String { + return "ReinitializeWallets(wallets = ${wallets.joinToString { it.walletId.toString() }}" + } + } + /** * Rename wallets * diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt index 726ed11fa8..db28dc4b12 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt @@ -51,7 +51,7 @@ internal class VisaWalletIntentsImplementor @Inject constructor( } override fun onBalancesAndLimitsClick() { - analyticsEventHandler.send(MainScreenAnalyticsEvent.LimitsClicked) + analyticsEventHandler.send(MainScreenAnalyticsEvent.LimitsClicked()) modelScope.launch(dispatchers.main) { val userWalletId = stateController.getSelectedWalletId() val balancesAndLimits = getVisaCurrencyUseCase(userWalletId) @@ -92,7 +92,7 @@ internal class VisaWalletIntentsImplementor @Inject constructor( } override fun onExploreClick(exploreUrl: String) { - analyticsEventHandler.send(MainScreenAnalyticsEvent.ButtonExplore) + analyticsEventHandler.send(MainScreenAnalyticsEvent.ButtonExplore()) router.openUrl(exploreUrl) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt index 466f15890e..ba738c15b9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt @@ -54,7 +54,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( ) : BaseWalletClickIntents(), WalletCardClickIntents { override fun onRenameBeforeConfirmationClick(userWalletId: UserWalletId) { - analyticsEventHandler.send(MainScreen.EditWalletTapped) + analyticsEventHandler.send(MainScreen.EditWalletTapped()) router.dialogNavigation.activate( configuration = WalletDialogConfig.RenameWallet( @@ -65,7 +65,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( } override fun onDeleteBeforeConfirmationClick(userWalletId: UserWalletId) { - analyticsEventHandler.send(MainScreen.DeleteWalletTapped) + analyticsEventHandler.send(MainScreen.DeleteWalletTapped()) walletEventSender.send( event = WalletEvent.ShowAlert( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index 4c0038bfe2..1802e3e2a7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -4,11 +4,12 @@ import arrow.core.getOrElse import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.tokens.TokenItemStateConverter.ApySource import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked @@ -205,11 +206,13 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( override fun onAccountExpandClick(account: Account) { val userWalletId = stateHolder.getSelectedWalletId() + analyticsEventHandler.send(MainScreenAnalyticsEvent.AccountShowTokens()) accountDependencies.expandedAccountsHolder.expandAccount(userWalletId, account.accountId) } override fun onAccountCollapseClick(account: Account) { val userWalletId = stateHolder.getSelectedWalletId() + analyticsEventHandler.send(MainScreenAnalyticsEvent.AccountHideTokens()) accountDependencies.expandedAccountsHolder.collapseAccount(userWalletId, account.accountId) } @@ -260,7 +263,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( token = currencyStatus.currency.symbol, blockchain = currencyStatus.currency.network.name, action = "Staking", - state = if (currencyStatus.value.yieldBalance is YieldBalance.Data) { + state = if (currencyStatus.value.stakingBalance is StakingBalance.Data) { "Enabled" } else { "Disabled" @@ -364,7 +367,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( is WalletNFTItemUM.Content -> { analyticsEventHandler.send( NFTAnalyticsEvent.NFTListScreenOpened( - state = NFTAnalyticsEvent.NFTListScreenOpened.State.Full, + state = AnalyticsParam.EmptyFull.Full, allAssetsCount = state.allAssetsCount, collectionsCount = state.collectionsCount, noCollectionAssetsCount = state.noCollectionAssetsCount, @@ -374,7 +377,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( is WalletNFTItemUM.Empty -> { analyticsEventHandler.send( NFTAnalyticsEvent.NFTListScreenOpened( - state = NFTAnalyticsEvent.NFTListScreenOpened.State.Empty, + state = AnalyticsParam.EmptyFull.Empty, allAssetsCount = 0, collectionsCount = 0, noCollectionAssetsCount = 0, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index 2572b3c5ff..aba0e7c275 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -11,9 +11,7 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.components.bottomsheets.message.* import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.card.SetCardWasScannedUseCase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.feedback.GetWalletMetaInfoUseCase @@ -33,14 +31,13 @@ import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.settings.NeverToSuggestRateAppUseCase import com.tangem.domain.settings.RemindToRateAppLaterUseCase 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.model.analytics.PromoAnalyticsEvent import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.Program import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.PromotionBannerClicked import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.models.UnlockWalletsError import com.tangem.domain.wallets.usecase.* -import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic @@ -106,7 +103,7 @@ internal interface WalletWarningsClickIntents { fun onDenyPermissions() - fun onFinishWalletActivationClick(bannerType: WalletActivationBannerType, isBackupExists: Boolean) + fun onFinishWalletActivationClick(isBackupExists: Boolean) } @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @@ -131,7 +128,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val urlOpener: UrlOpener, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, private val stakingIdFactory: StakingIdFactory, private val appRouter: AppRouter, private val hotWalletFeatureToggles: HotWalletFeatureToggles, @@ -144,7 +141,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( ) : BaseWalletClickIntents(), WalletWarningsClickIntents { override fun onAddBackupCardClick() { - analyticsEventHandler.send(MainScreen.NoticeBackupYourWalletTapped) + analyticsEventHandler.send(MainScreen.NoticeBackupYourWalletTapped()) prepareAndStartOnboardingProcess() } @@ -172,7 +169,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onGenerateMissedAddressesClick(missedAddressCurrencies: List) { analyticsEventHandler.send(Basic.CardWasScanned(AnalyticsParam.ScreensSources.Main)) - analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped) + analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped()) modelScope.launch { val userWallet = getSelectedUserWallet() ?: return@launch @@ -190,7 +187,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } override fun onOpenUnlockWalletsBottomSheetClick() { - analyticsEventHandler.send(MainScreen.WalletUnlockTapped) + analyticsEventHandler.send(MainScreen.WalletUnlockTapped()) if (hotWalletFeatureToggles.isHotWalletEnabled) { modelScope.launch { @@ -202,6 +199,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( error.handle( onAlreadyUnlocked = {}, onUserCancelled = {}, + analyticsEventHandler = analyticsEventHandler, showMessage = uiMessageSender::send, ) } @@ -221,7 +219,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( @Deprecated("Will be removed with hot wallet release") override fun onUnlockWalletClick() { - analyticsEventHandler.send(MainScreen.UnlockAllWithBiometrics) + analyticsEventHandler.send(MainScreen.UnlockAllWithBiometrics()) modelScope.launch(dispatchers.main) { unlockWalletsUseCase(type = UnlockType.ALL_WITHOUT_SELECT) @@ -249,7 +247,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( @Deprecated("Will be removed with hot wallet release") override fun onScanToUnlockWalletClick() { - analyticsEventHandler.send(MainScreen.UnlockWithCardScan) + analyticsEventHandler.send(MainScreen.UnlockWithCardScan()) openScanCardDialog() } @@ -307,22 +305,22 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onClosePromoClick(promoId: PromoId) { analyticsEventHandler.send( when (promoId) { - PromoId.Referral -> MainScreen.ReferralPromoButtonDismiss + PromoId.Referral -> MainScreen.ReferralPromoButtonDismiss() PromoId.Sepa -> PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Main, program = Program.Sepa, - action = PromotionBannerClicked.BannerAction.Closed, + action = PromotionBannerClicked.BannerAction.Closed(), ) - PromoId.VisaPresale -> PromoAnalyticsEvent.VisaWaitlistPromoDismiss + PromoId.VisaPresale -> PromoAnalyticsEvent.VisaWaitlistPromoDismiss() PromoId.BlackFriday -> PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Main, program = Program.BlackFriday, - action = PromotionBannerClicked.BannerAction.Closed, + action = PromotionBannerClicked.BannerAction.Closed(), ) PromoId.OnePlusOne -> PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Main, program = Program.OnePlusOne, - action = PromotionBannerClicked.BannerAction.Closed, + action = PromotionBannerClicked.BannerAction.Closed(), ) }, @@ -336,7 +334,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( val userWallet = getSelectedUserWallet() ?: return when (promoId) { PromoId.Referral -> { - analyticsEventHandler.send(MainScreen.ReferralPromoButtonParticipate) + analyticsEventHandler.send(MainScreen.ReferralPromoButtonParticipate()) appRouter.push(ReferralProgram(userWalletId = userWallet.walletId)) } PromoId.Sepa -> { @@ -344,7 +342,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Main, program = Program.Sepa, - action = PromotionBannerClicked.BannerAction.Clicked, + action = PromotionBannerClicked.BannerAction.Clicked(), ), ) cryptoCurrency ?: return @@ -358,7 +356,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( ) } PromoId.VisaPresale -> { - analyticsEventHandler.send(PromoAnalyticsEvent.VisaWaitlistPromoJoin) + analyticsEventHandler.send(PromoAnalyticsEvent.VisaWaitlistPromoJoin()) urlOpener.openUrl(VISA_PROMO_LINK) } PromoId.BlackFriday -> { @@ -366,7 +364,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Main, program = Program.BlackFriday, - action = PromotionBannerClicked.BannerAction.Clicked, + action = PromotionBannerClicked.BannerAction.Clicked(), ), ) urlOpener.openUrl(BLACK_FRIDAY_PROMO_LINK) @@ -376,7 +374,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Main, program = Program.OnePlusOne, - action = PromotionBannerClicked.BannerAction.Clicked, + action = PromotionBannerClicked.BannerAction.Clicked(), ), ) urlOpener.openUrl(ONE_PLUS_ONE_PROMO_LINK) @@ -394,7 +392,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } override fun onNoteMigrationButtonClick(url: String) { - analyticsEventHandler.send(MainScreen.NotePromoButton) + analyticsEventHandler.send(MainScreen.NotePromoButton()) modelScope.launch(dispatchers.main) { router.openUrl(url) } @@ -403,7 +401,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onSeedPhraseNotificationConfirm() { val userWallet = getSelectedUserWallet() ?: return - analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonYes) + analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonYes()) walletEventSender.send( event = WalletEvent.ShowAlert( @@ -426,7 +424,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onSeedPhraseNotificationDecline() { val userWallet = getSelectedUserWallet() ?: return - analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonNo) + analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonNo()) walletEventSender.send( event = WalletEvent.ShowAlert( @@ -445,7 +443,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onSeedPhraseSecondNotificationAccept() { val userWallet = getSelectedUserWallet() ?: return - analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonUsed) + analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonUsed()) walletEventSender.send( event = WalletEvent.ShowAlert( @@ -468,51 +466,21 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onSeedPhraseSecondNotificationReject() { val userWallet = getSelectedUserWallet() ?: return - analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonDeclined) + analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonDeclined()) modelScope.launch { seedPhraseNotificationUseCase.rejectSecond(userWalletId = userWallet.walletId) } } - override fun onFinishWalletActivationClick(bannerType: WalletActivationBannerType, isBackupExists: Boolean) { - when (bannerType) { - WalletActivationBannerType.Attention -> { - val userWallet = getSelectedUserWallet() ?: return - appRouter.push(WalletActivation(userWallet.walletId, isBackupExists)) - } - WalletActivationBannerType.Warning -> { - val message = bottomSheetMessage { - infoBlock { - icon(R.drawable.img_knight_shield_32) { - type = MessageBottomSheetUMV2.Icon.Type.Warning - backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint - } - title = resourceReference(R.string.hw_activation_need_title) - body = resourceReference(R.string.hw_activation_need_description) - } - secondaryButton { - text = resourceReference(R.string.common_later) - onClick { - closeBs() - } - } - primaryButton { - text = resourceReference(R.string.hw_activation_need_backup) - onClick { - val userWallet = getSelectedUserWallet() ?: return@onClick - appRouter.push(WalletActivation(userWallet.walletId, isBackupExists)) - closeBs() - } - } - } - uiMessageSender.send(message) - } - } + override fun onFinishWalletActivationClick(isBackupExists: Boolean) { + analyticsEventHandler.send(MainScreen.ButtonFinalizeActivation()) + val userWalletId = stateHolder.getSelectedWalletId() + appRouter.push(WalletActivation(userWalletId, isBackupExists)) } override fun onAllowPermissions() { - analyticsEventHandler.send(WalletScreenAnalyticsEvent.PushBannerPromo.ButtonAllowPush) + analyticsEventHandler.send(WalletScreenAnalyticsEvent.PushBannerPromo.ButtonAllowPush()) walletEventSender.send( event = WalletEvent.RequestPushPermissions( onAllow = { @@ -538,7 +506,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onDenyPermissions() { modelScope.launch { - analyticsEventHandler.send(WalletScreenAnalyticsEvent.PushBannerPromo.ButtonLaterPush) + analyticsEventHandler.send(WalletScreenAnalyticsEvent.PushBannerPromo.ButtonLaterPush()) updateSubscribeOnPushPermissions(false) } } @@ -577,8 +545,11 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() } - multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), + multiStakingBalanceFetcher( + params = MultiStakingBalanceFetcher.Params( + userWalletId = userWalletId, + stakingIds = stakingIds, + ), ) .onLeft { Timber.e("Unable to fetch yield balances: $it") } }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt index e7f0ce0a1f..5c151cb3ef 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt @@ -49,7 +49,7 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( ) : PromoDeeplinkHandler { init { - analyticsEventsHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) + analyticsEventsHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart()) val promoCode = queryParams[PROMO_CODE_KEY].orEmpty() if (promoCode.isEmpty()) { showAlert(InvalidPromoCode) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/analytics/PromoActivationAnalytics.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/analytics/PromoActivationAnalytics.kt index bd1bbc7dcc..ff1f1e2c1f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/analytics/PromoActivationAnalytics.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/analytics/PromoActivationAnalytics.kt @@ -8,7 +8,7 @@ sealed class PromoActivationAnalytics( params: Map = mapOf(), ) : AnalyticsEvent(category = "Promotion", event = event, params = params) { - data object PromoDeepLinkActivationStart : PromoActivationAnalytics( + class PromoDeepLinkActivationStart : PromoActivationAnalytics( event = "Bitcoin Promo Deep Link Activation", params = emptyMap(), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index 3050924d0c..e617d2d783 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.common.preview import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.components.token.AccountItemPreviewData import com.tangem.core.ui.components.token.state.TokenItemState @@ -14,6 +13,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarConfig import com.tangem.feature.wallet.presentation.wallet.state.model.* @@ -167,11 +167,12 @@ internal object WalletScreenPreviewData { warnings = persistentListOf( WalletNotification.Warning.SomeNetworksUnreachable, WalletNotification.FinishWalletActivation( - iconTint = NotificationConfig.IconTint.Attention, + type = WalletActivationBannerType.Attention, buttonsState = ButtonsState.SecondaryButtonConfig( text = resourceReference(R.string.hw_activation_need_finish), onClick = { }, ), + isBackupExists = false, ), ), bottomSheetConfig = null, @@ -222,6 +223,7 @@ internal object WalletScreenPreviewData { isHidingMode = false, showMarketsOnboarding = false, onDismissMarketsOnboarding = {}, + isNewMarketEnabled = false, ) internal val accountScreenState = diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt index 4d5ccf9b2c..a0c96c2974 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt @@ -8,11 +8,11 @@ sealed class PortfolioOrganizeTokensAnalyticsEvent( params: Map = mapOf(), ) : AnalyticsEvent("Portfolio / Organize Tokens", event, params) { - object ScreenOpened : PortfolioOrganizeTokensAnalyticsEvent("Organize Tokens Screen Opened") + class ScreenOpened : PortfolioOrganizeTokensAnalyticsEvent("Organize Tokens Screen Opened") - object ByBalance : PortfolioOrganizeTokensAnalyticsEvent("Button - By Balance") + class ByBalance : PortfolioOrganizeTokensAnalyticsEvent("Button - By Balance") - object Group : PortfolioOrganizeTokensAnalyticsEvent("Button - Group") + class Group : PortfolioOrganizeTokensAnalyticsEvent("Button - Group") class Apply( grouping: AnalyticsParam.OnOffState, @@ -25,5 +25,5 @@ sealed class PortfolioOrganizeTokensAnalyticsEvent( ), ) - object Cancel : PortfolioOrganizeTokensAnalyticsEvent("Button - Cancel") + class Cancel : PortfolioOrganizeTokensAnalyticsEvent("Button - Cancel") } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 21f42375ef..4955e962ed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -8,7 +8,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId @@ -63,12 +63,12 @@ internal class CryptoCurrencyToDraggableItemConverter( } private fun getFormattedFiatAmount(currency: CryptoCurrencyStatus, appCurrency: AppCurrency): String { - val yieldBalance = currency.value.yieldBalance as? YieldBalance.Data + val stakingBalance = currency.value.stakingBalance as? StakingBalance.Data val fiatRate = currency.value.fiatRate ?: BigDecimal.ZERO - val fiatYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.rawId) + val fiatStakingBalance = stakingBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.rawId) ?.multiply(fiatRate).orZero() val fiatAmount = currency.value.fiatAmount ?: return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN - return (fiatAmount + fiatYieldBalance).format { fiat(appCurrency.code, appCurrency.symbol) } + return (fiatAmount + fiatStakingBalance).format { fiat(appCurrency.code, appCurrency.symbol) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt index 8a8dc8252b..2851d268b6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt @@ -9,7 +9,7 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId @@ -56,12 +56,12 @@ internal class CryptoCurrencyToDraggableItemConverterV2( } private fun getFormattedFiatAmount(currency: CryptoCurrencyStatus, appCurrency: AppCurrency): String { - val yieldBalance = currency.value.yieldBalance as? YieldBalance.Data + val stakingBalance = currency.value.stakingBalance as? StakingBalance.Data val fiatRate = currency.value.fiatRate ?: BigDecimal.ZERO - val fiatYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.rawId) + val fiatStakingBalance = stakingBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.rawId) ?.multiply(fiatRate).orZero() val fiatAmount = currency.value.fiatAmount ?: return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN - return (fiatAmount + fiatYieldBalance).format { fiat(appCurrency.code, appCurrency.symbol) } + return (fiatAmount + fiatStakingBalance).format { fiat(appCurrency.code, appCurrency.symbol) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index 54d3a4cca2..317c4582c9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -37,7 +37,7 @@ sealed class WalletScreenAnalyticsEvent { }, ) - class TokenBalance(balance: AnalyticsParam.TokenBalanceState, token: String) : Basic( + class TokenBalance(balance: AnalyticsParam.EmptyFull, token: String) : Basic( event = "Token Balance", params = mapOf( AnalyticsParam.STATE to balance.value, @@ -51,7 +51,40 @@ sealed class WalletScreenAnalyticsEvent { params: Map = mapOf(), ) : AnalyticsEvent(category = "Main Screen", event = event, params = params) { - data object ScreenOpened : MainScreen(event = "Screen opened") + class ScreenOpenedLegacy : MainScreen( + event = "Screen opened", + ) + + data class ScreenOpened( + private val hasMobileWallet: Boolean, + private val accountsCount: Int?, + ) : MainScreen( + event = "Screen opened", + params = buildMap { + put("Mobile Wallet", if (hasMobileWallet) "Yes" else "No") + if (accountsCount != null) put("Accounts Count", accountsCount.toString()) + }, + ) + + data class NoticeFinishActivation( + private val activationState: ActivationState, + private val balanceState: AnalyticsParam.EmptyFull, + ) : MainScreen( + event = "Notice - Finish Activation", + params = mapOf( + "Activation State" to activationState.value, + "Balance State" to balanceState.value, + ), + ) { + enum class ActivationState(val value: String) { + NotStarted("Not Started"), + Unfinished("Unfinished"), + } + } + + class ButtonFinalizeActivation : MainScreen( + event = "Button - Finalize Activation", + ) class WalletSelected(val isImported: Boolean) : MainScreen( event = "Wallet Selected", @@ -70,9 +103,9 @@ sealed class WalletScreenAnalyticsEvent { params = mapOf("Result" to result.value), ) - data object NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped") - data object NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped") - data object WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped") + class NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped") + class NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped") + class WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped") class NetworksUnreachable( tokens: List, @@ -81,54 +114,54 @@ sealed class WalletScreenAnalyticsEvent { params = mapOf("Tokens" to tokens.joinToString()), ) - data object MissingAddresses : MainScreen(event = "Notice - Missing Addresses") + class MissingAddresses : MainScreen(event = "Notice - Missing Addresses") - data object CardSignedTransactions : MainScreen(event = "Notice - Card Signed Transactions") + class CardSignedTransactions : MainScreen(event = "Notice - Card Signed Transactions") - data object HowDoYouLikeTangem : MainScreen(event = "Notice - How Do You Like Tangem") + class HowDoYouLikeTangem : MainScreen(event = "Notice - How Do You Like Tangem") - data object ProductSampleCard : MainScreen(event = "Notice - Product Sample Card") + class ProductSampleCard : MainScreen(event = "Notice - Product Sample Card") - data object TestnetCard : MainScreen(event = "Notice - Testnet Card") + class TestnetCard : MainScreen(event = "Notice - Testnet Card") - data object DemoCard : MainScreen(event = "Notice - Demo Card") + class DemoCard : MainScreen(event = "Notice - Demo Card") - data object DevelopmentCard : MainScreen(event = "Notice - Development Card") + class DevelopmentCard : MainScreen(event = "Notice - Development Card") - data object WalletUnlock : MainScreen(event = "Notice - Wallet Unlock") + class WalletUnlock : MainScreen(event = "Notice - Wallet Unlock") - data object BackupYourWallet : MainScreen(event = "Notice - Backup Your Wallet") + class BackupYourWallet : MainScreen(event = "Notice - Backup Your Wallet") - data object BackupError : MainScreen(event = "Notice - Backup Error") + class BackupError : MainScreen(event = "Notice - Backup Error") - data object NotePromo : MainScreen(event = "Notice - Note Promo") + class NotePromo : MainScreen(event = "Notice - Note Promo") - data object NotePromoButton : MainScreen(event = "Note Promo Button") + class NotePromoButton : MainScreen(event = "Note Promo Button") - data object UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics") + class UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics") - data object UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan") + class UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan") - data object EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped") + class EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped") - data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped") + class DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped") - data object NoticeSeedPhraseSupport : MainScreen(event = "Notice - Seed Phrase Support") + class NoticeSeedPhraseSupport : MainScreen(event = "Notice - Seed Phrase Support") - data object NoticeSeedPhraseSupportSecond : MainScreen(event = "Notice - Seed Phrase Support2") + class NoticeSeedPhraseSupportSecond : MainScreen(event = "Notice - Seed Phrase Support2") - data object NoticeSeedPhraseSupportButtonNo : MainScreen(event = "Button - Support No") + class NoticeSeedPhraseSupportButtonNo : MainScreen(event = "Button - Support No") - data object NoticeSeedPhraseSupportButtonYes : MainScreen(event = "Button - Support Yes") + class NoticeSeedPhraseSupportButtonYes : MainScreen(event = "Button - Support Yes") - data object NoticeSeedPhraseSupportButtonUsed : MainScreen(event = "Button - Support Used") + class NoticeSeedPhraseSupportButtonUsed : MainScreen(event = "Button - Support Used") - data object NoticeSeedPhraseSupportButtonDeclined : MainScreen(event = "Button - Support Declined") + class NoticeSeedPhraseSupportButtonDeclined : MainScreen(event = "Button - Support Declined") // region Referral Promo - data object ReferralPromo : MainScreen(event = "Referral Banner") - data object ReferralPromoButtonParticipate : MainScreen(event = "Button - Referral Participate") - data object ReferralPromoButtonDismiss : MainScreen(event = "Button - Referral Dismiss") + class ReferralPromo : MainScreen(event = "Referral Banner") + class ReferralPromoButtonParticipate : MainScreen(event = "Button - Referral Participate") + class ReferralPromoButtonDismiss : MainScreen(event = "Button - Referral Dismiss") //endregion } @@ -137,8 +170,8 @@ sealed class WalletScreenAnalyticsEvent { params: Map = mapOf(), ) : AnalyticsEvent(category = "Promo", event = event, params = params) { - data object PushBanner : PushBannerPromo(event = "Push Banner") - data object ButtonAllowPush : PushBannerPromo(event = "Button - Allow Push") - data object ButtonLaterPush : PushBannerPromo(event = "Button - Later Push") + class PushBanner : PushBannerPromo(event = "Push Banner") + class ButtonAllowPush : PushBannerPromo(event = "Button - Allow Push") + class ButtonLaterPush : PushBannerPromo(event = "Button - Later Push") } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/SelectedWalletAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/SelectedWalletAnalyticsSender.kt index 90e94e979a..ad5cc6aa22 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/SelectedWalletAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/SelectedWalletAnalyticsSender.kt @@ -28,7 +28,7 @@ internal class SelectedWalletAnalyticsSender @Inject constructor( * that cannot be processed in [WalletWarningsAnalyticsSender]. * */ private fun getEvent(userWallet: UserWallet): AnalyticsEvent? = when { - userWallet.isLocked -> WalletScreenAnalyticsEvent.MainScreen.WalletUnlock + userWallet.isLocked -> WalletScreenAnalyticsEvent.MainScreen.WalletUnlock() else -> WalletScreenAnalyticsEvent.MainScreen.WalletSelected(userWallet.isImported()) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt index fe583f7183..b8d4f81c17 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -156,9 +156,9 @@ internal class TokenListAnalyticsSender @Inject constructor( -> { if (balanceWasSentMap[blockchain.currency] != true) { val tokenBalance = if (balanceStatus.amount.isZero()) { - AnalyticsParam.TokenBalanceState.Empty + AnalyticsParam.EmptyFull.Empty } else { - AnalyticsParam.TokenBalanceState.Full + AnalyticsParam.EmptyFull.Full } analyticsEventHandler.send( Basic.TokenBalance( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index c56c99c1b2..94ede58c14 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -5,6 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.* +import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification @@ -39,16 +40,16 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( @Suppress("CyclomaticComplexMethod") private fun getEvent(warning: WalletNotification): AnalyticsEvent? { return when (warning) { - is WalletNotification.Critical.DevCard -> MainScreen.DevelopmentCard - is WalletNotification.Critical.FailedCardValidation -> MainScreen.ProductSampleCard - is WalletNotification.Warning.MissingBackup -> MainScreen.BackupYourWallet - is WalletNotification.Warning.NumberOfSignedHashesIncorrect -> MainScreen.CardSignedTransactions - is WalletNotification.Warning.TestNetCard -> MainScreen.TestnetCard - is WalletNotification.Informational.DemoCard -> MainScreen.DemoCard - is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses - is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem - is WalletNotification.Critical.BackupError -> MainScreen.BackupError - is WalletNotification.NoteMigration -> MainScreen.NotePromo + is WalletNotification.Critical.DevCard -> MainScreen.DevelopmentCard() + is WalletNotification.Critical.FailedCardValidation -> MainScreen.ProductSampleCard() + is WalletNotification.Warning.MissingBackup -> MainScreen.BackupYourWallet() + is WalletNotification.Warning.NumberOfSignedHashesIncorrect -> MainScreen.CardSignedTransactions() + is WalletNotification.Warning.TestNetCard -> MainScreen.TestnetCard() + is WalletNotification.Informational.DemoCard -> MainScreen.DemoCard() + is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses() + is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem() + is WalletNotification.Critical.BackupError -> MainScreen.BackupError() + is WalletNotification.NoteMigration -> MainScreen.NotePromo() is WalletNotification.SwapPromo -> NoticePromotionBanner( source = AnalyticsParam.ScreensSources.Main, program = Program.Empty, // Use it on new promo action @@ -65,8 +66,8 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( source = AnalyticsParam.ScreensSources.Main, program = Program.OnePlusOne, ) - is WalletNotification.ReferralPromo -> MainScreen.ReferralPromo - is WalletNotification.VisaPresalePromo -> VisaWaitlistPromo + is WalletNotification.ReferralPromo -> MainScreen.ReferralPromo() + is WalletNotification.VisaPresalePromo -> VisaWaitlistPromo() is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender] is WalletNotification.Informational.NoAccount, is WalletNotification.Warning.LowSignatures, @@ -74,12 +75,26 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.Warning.NetworksUnreachable, is WalletNotification.UsedOutdatedData, is WalletNotification.UnlockVisaAccess, - is WalletNotification.FinishWalletActivation, is WalletNotification.Warning.YeildSupplyApprove, // TODO apply correct event -> null - is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport - is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond - is WalletNotification.PushNotifications -> WalletScreenAnalyticsEvent.PushBannerPromo.PushBanner + is WalletNotification.FinishWalletActivation -> { + val activationState = if (warning.isBackupExists) { + MainScreen.NoticeFinishActivation.ActivationState.Unfinished + } else { + MainScreen.NoticeFinishActivation.ActivationState.NotStarted + } + val balanceState = when (warning.type) { + WalletActivationBannerType.Attention -> AnalyticsParam.EmptyFull.Empty + WalletActivationBannerType.Warning -> AnalyticsParam.EmptyFull.Full + } + MainScreen.NoticeFinishActivation( + activationState = activationState, + balanceState = balanceState, + ) + } + is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport() + is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond() + is WalletNotification.PushNotifications -> WalletScreenAnalyticsEvent.PushBannerPromo.PushBanner() is WalletNotification.Warning.TangemPayRefreshNeeded -> null WalletNotification.Warning.TangemPayUnreachable -> null } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt index 7bf7785b0a..a51f2133e4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt @@ -1,8 +1,24 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils +import arrow.atomic.AtomicBoolean +import com.tangem.common.routing.AppRoute.WalletActivation import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 +import com.tangem.core.ui.components.bottomsheets.message.icon +import com.tangem.core.ui.components.bottomsheets.message.infoBlock +import com.tangem.core.ui.components.bottomsheets.message.onClick +import com.tangem.core.ui.components.bottomsheets.message.primaryButton +import com.tangem.core.ui.components.bottomsheets.message.secondaryButton +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.bottomSheetMessage +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase +import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider @@ -12,8 +28,13 @@ import javax.inject.Inject internal class WalletWarningsSingleEventSender @Inject constructor( private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase, private val screenLifecycleProvider: ScreenLifecycleProvider, + private val uiMessageSender: UiMessageSender, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val router: Router, ) { + private val isActivationBottomSheetShown: AtomicBoolean = AtomicBoolean(false) + suspend fun send( userWalletId: UserWalletId, displayedUiState: WalletState?, @@ -26,9 +47,49 @@ internal class WalletWarningsSingleEventSender @Inject constructor( val events = newWarnings.filter { it !in displayedUiState.warnings } events.forEach { event -> - if (event is WalletNotification.Critical.SeedPhraseNotification) { - seedPhraseNotificationUseCase.notified(userWalletId = userWalletId) + when (event) { + is WalletNotification.Critical.SeedPhraseNotification -> { + seedPhraseNotificationUseCase.notified(userWalletId = userWalletId) + } + is WalletNotification.FinishWalletActivation -> { + if (event.type == WalletActivationBannerType.Warning && !isActivationBottomSheetShown.get()) { + showFinishActivationBottomSheet(userWalletId) + } + isActivationBottomSheetShown.set(true) + } + else -> Unit } } } + + private fun showFinishActivationBottomSheet(userWalletId: UserWalletId) { + val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return + if (userWallet !is UserWallet.Hot) return + + val message = bottomSheetMessage { + infoBlock { + icon(R.drawable.img_knight_shield_32) { + type = MessageBottomSheetUMV2.Icon.Type.Warning + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.hw_activation_need_title) + body = resourceReference(R.string.hw_activation_need_description) + } + secondaryButton { + text = resourceReference(R.string.common_later) + onClick { + closeBs() + } + } + primaryButton { + text = resourceReference(R.string.hw_activation_need_backup) + onClick { + router.push(WalletActivation(userWallet.walletId, userWallet.backedUp)) + closeBs() + } + } + } + + uiMessageSender.send(message) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 010639c906..b757338349 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -6,7 +6,6 @@ import com.tangem.common.ui.notifications.NotificationId import com.tangem.common.ui.userwallet.ext.walletInterationIcon import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState -import com.tangem.core.ui.components.notifications.NotificationConfig.IconTint import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.card.CardTypesResolver @@ -36,6 +35,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificat import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.extensions.addIf import com.tangem.utils.extensions.isPositive +import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -59,7 +59,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase, ) { - @Suppress("UNCHECKED_CAST", "MagicNumber") + @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod") fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver @@ -115,7 +115,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( addFinishWalletActivationNotification( userWallet = userWallet, - totalFiatBalance = totalFiatBalance, + flattenCurrencies = flattenCurrencies, clickIntents = clickIntents, shouldAccessCodeSkipped = shouldAccessCodeSkipped, ) @@ -361,8 +361,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun TotalFiatBalance?.getFinishWalletActivationType(): WalletActivationBannerType { - return if ((this as? TotalFiatBalance.Loaded)?.amount?.isPositive() == true) { + private fun List?.getFinishWalletActivationType(): WalletActivationBannerType { + return if (this?.any { it.value.amount.orZero().isPositive() } == true) { WalletActivationBannerType.Warning } else { WalletActivationBannerType.Attention @@ -371,7 +371,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun MutableList.addFinishWalletActivationNotification( userWallet: UserWallet, - totalFiatBalance: Lce, + flattenCurrencies: Lce>, clickIntents: WalletClickIntents, shouldAccessCodeSkipped: Boolean, ) { @@ -382,30 +382,26 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( !shouldAccessCodeSkipped val shouldShowFinishActivation = !isBackupExists || isAccessCodeRequired - val type = totalFiatBalance.fold( - ifLoading = { it.getFinishWalletActivationType() }, + val type = flattenCurrencies.fold( + ifLoading = { return }, ifContent = { it.getFinishWalletActivationType() }, ifError = { WalletActivationBannerType.Attention }, ) - val tint = when (type) { - WalletActivationBannerType.Attention -> IconTint.Attention - WalletActivationBannerType.Warning -> IconTint.Warning - } - addIf( element = WalletNotification.FinishWalletActivation( - iconTint = tint, + type = type, buttonsState = when (type) { WalletActivationBannerType.Warning -> ButtonsState.PrimaryButtonConfig( text = resourceReference(R.string.hw_activation_need_finish), - onClick = { clickIntents.onFinishWalletActivationClick(type, isBackupExists) }, + onClick = { clickIntents.onFinishWalletActivationClick(isBackupExists) }, ) else -> ButtonsState.SecondaryButtonConfig( text = resourceReference(R.string.hw_activation_need_finish), - onClick = { clickIntents.onFinishWalletActivationClick(type, isBackupExists) }, + onClick = { clickIntents.onFinishWalletActivationClick(isBackupExists) }, ) }, + isBackupExists = isBackupExists, ), condition = shouldShowFinishActivation, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt index 31fffb5d04..b911a9a509 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt @@ -50,6 +50,7 @@ internal object WalletAdditionalInfoFactory { backedUp.not() -> DIVIDER + TextReference.Res(R.string.hw_backup_no_backup) else -> TextReference.Str("") }, + isHotBackedUp = backedUp, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index de8c2ee08a..572e187d6b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -110,6 +110,7 @@ internal class WalletStateController @Inject constructor() { isHidingMode = false, showMarketsOnboarding = false, onDismissMarketsOnboarding = {}, + isNewMarketEnabled = false, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAdditionalInfo.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAdditionalInfo.kt index 2e210c2221..a12cd21993 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAdditionalInfo.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAdditionalInfo.kt @@ -7,4 +7,5 @@ import com.tangem.core.ui.extensions.TextReference data class WalletAdditionalInfo( val hideable: Boolean, val content: TextReference, + val isHotBackedUp: Boolean = false, ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index 22028e514d..73a2e96b23 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -10,6 +10,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.impl.R import org.joda.time.DateTime @@ -287,14 +288,22 @@ sealed class WalletNotification(val config: NotificationConfig) { ) data class FinishWalletActivation( - val iconTint: IconTint, + val type: WalletActivationBannerType, val buttonsState: ButtonsState, + val isBackupExists: Boolean, ) : WalletNotification( config = NotificationConfig( title = resourceReference(R.string.hw_activation_need_title), - subtitle = resourceReference(R.string.hw_activation_need_description), + subtitle = if (isBackupExists) { + resourceReference(R.string.hw_activation_need_warning_description) + } else { + resourceReference(R.string.hw_activation_need_description) + }, iconResId = R.drawable.img_knight_shield_32, - iconTint = iconTint, + iconTint = when (type) { + WalletActivationBannerType.Attention -> IconTint.Attention + WalletActivationBannerType.Warning -> IconTint.Warning + }, buttonsState = buttonsState, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt index df260b5856..1fb6a6d24c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt @@ -13,5 +13,6 @@ internal data class WalletScreenState( val event: StateEvent, val isHidingMode: Boolean, val showMarketsOnboarding: Boolean, + val isNewMarketEnabled: Boolean, val onDismissMarketsOnboarding: () -> Unit, ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt index 0c3ac24ecd..9e11d7cd7e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt @@ -137,7 +137,7 @@ internal class SetVisaInfoTransformer( fiatAmount = visaCurrency.balances.available.multiply(visaCurrency.fiatRate), fiatRate = visaCurrency.fiatRate, priceChange = visaCurrency.priceChange, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -154,7 +154,7 @@ internal class SetVisaInfoTransformer( clickIntents.onReceiveClick( userWalletId = userWalletId, cryptoCurrencyStatus = cryptoCurrencyStatus, - event = MainScreenAnalyticsEvent.ButtonReceive, + event = MainScreenAnalyticsEvent.ButtonReceive(), ) }, onLongClick = { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt index eb2e240d23..4a2af49f86 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt @@ -44,12 +44,12 @@ internal class BalancesAndLimitsBottomSheetConverter( } private fun balanceInfoOnClick() { - analyticsEventHandler.send(MainScreenAnalyticsEvent.NoticeBalancesInfo) + analyticsEventHandler.send(MainScreenAnalyticsEvent.NoticeBalancesInfo()) eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaBalancesInfo)) } private fun limitInfoOnClick(totalLimit: String, otherLimit: String) { - analyticsEventHandler.send(MainScreenAnalyticsEvent.NoticeLimitsInfo) + analyticsEventHandler.send(MainScreenAnalyticsEvent.NoticeLimitsInfo()) eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaLimitsInfo(totalLimit, otherLimit))) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt index 73813e73d3..3df7db2bb6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt @@ -82,9 +82,9 @@ internal class SingleWalletOnrampTransactionConverter( fallbackResId = R.drawable.ic_currency_24, ), iconState = getIconState(value.status), - onGoToProviderClick = { - analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) - clickIntents.onGoToProviderClick(it) + onGoToProviderClick = { url -> + analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider()) + clickIntents.onGoToProviderClick(url) }, onDisposeExpressStatus = clickIntents::onConfirmDisposeExpressStatus, onClick = { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt index 080766b9b3..ae2648f6d7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt @@ -51,7 +51,6 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { stakingApyMap: Map> = emptyMap(), shouldShowMainPromo: Boolean = false, ) { - val accountFlattenCurrencies = accountList.flattenCurrencies() val mainAccount = accountList.mainAccount when { @@ -73,26 +72,14 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { ) } isAccountMode -> { - val isAllAccountsEmpty = accountFlattenCurrencies.isEmpty() - if (isAllAccountsEmpty) { - stateController.update( - SetTokenListErrorTransformer( - selectedWallet = userWallet, - error = TokenListError.EmptyTokens, - appCurrency = appCurrency, - ), - ) - } else { - val convertParams = TokenConverterParams.Account(accountList, expandedAccounts) - - updateContent( - params = convertParams, - appCurrency = appCurrency, - yieldSupplyApyMap = yieldSupplyApyMap, - stakingApyMap = stakingApyMap, - shouldShowMainPromo = shouldShowMainPromo, - ) - } + val convertParams = TokenConverterParams.Account(accountList, expandedAccounts) + updateContent( + params = convertParams, + appCurrency = appCurrency, + yieldSupplyApyMap = yieldSupplyApyMap, + stakingApyMap = stakingApyMap, + shouldShowMainPromo = shouldShowMainPromo, + ) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 16212dc865..1f2e8441c2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -51,6 +51,7 @@ import com.tangem.common.ui.expressStatus.expressTransactionsItems import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.components.atoms.handComposableComponentHeight import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer import com.tangem.core.ui.components.rememberIsKeyboardVisible import com.tangem.core.ui.components.sheetscaffold.* @@ -87,15 +88,18 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPa import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.VisaTxDetailsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balancesAndLimitsBlock import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator -import com.tangem.features.markets.entry.BottomSheetState -import com.tangem.features.markets.entry.MarketsEntryComponent import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.math.roundToInt @Composable -internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: MarketsEntryComponent) { +internal fun WalletScreen( + state: WalletScreenState, + bottomSheetContent: @Composable (() -> Unit), + bottomSheetHeaderHeightProvider: () -> Dp, + onBottomSheetStateChange: (BottomSheetState) -> Unit, +) { // It means that screen is still initializing if (state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX) return @@ -116,8 +120,10 @@ internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: Marke snackbarHostState = snackbarHostState, isAutoScroll = isAutoScroll, onAutoScrollReset = { isAutoScroll.value = false }, - marketsEntryComponent = marketsEntryComponent, + bottomSheetContent = bottomSheetContent, alertConfig = alertConfig, + bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, + onBottomSheetStateChange = onBottomSheetStateChange, ) WalletEventEffect( @@ -136,9 +142,11 @@ private fun WalletContent( walletsListState: LazyListState, snackbarHostState: SnackbarHostState, isAutoScroll: State, - marketsEntryComponent: MarketsEntryComponent, alertConfig: WalletAlertState?, onAutoScrollReset: () -> Unit, + bottomSheetHeaderHeightProvider: () -> Dp, + onBottomSheetStateChange: (BottomSheetState) -> Unit, + bottomSheetContent: @Composable (() -> Unit), ) { /* * Don't pass key to remember, because it will brake scroll animation. @@ -286,25 +294,15 @@ private fun WalletContent( ) } - val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) } - - var headerSize by remember { mutableStateOf(0.dp) } - BaseScaffoldWithMarkets( state = state, listState = listState, selectedWallet = selectedWallet, snackbarHostState = snackbarHostState, - bottomSheetHeaderHeightProvider = { headerSize }, + bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, alertConfig = alertConfig, - onBottomSheetStateChange = { bottomSheetState.value = it }, - bottomSheetContent = { - marketsEntryComponent.BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = { headerSize = it }, - modifier = Modifier, - ) - }, + onBottomSheetStateChange = onBottomSheetStateChange, + bottomSheetContent = bottomSheetContent, content = scaffoldContent, ) } @@ -394,7 +392,11 @@ private inline fun BaseScaffoldWithMarkets( val maxHeight = LocalWindowSize.current.height val coroutineScope = rememberCoroutineScope() - val backgroundPrimary = TangemTheme.colors.background.primary + val background = if (state.isNewMarketEnabled) { + TangemTheme.colors.background.tertiary + } else { + TangemTheme.colors.background.primary + } val showMarketsHint by remember { derivedStateOf { @@ -407,7 +409,7 @@ private inline fun BaseScaffoldWithMarkets( } CompositionLocalProvider( - LocalMainBottomSheetColor provides remember { mutableStateOf(backgroundPrimary) }, + LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, ) { val backgroundColor = LocalMainBottomSheetColor.current var isSearchFieldFocused by remember { mutableStateOf(false) } @@ -798,16 +800,11 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider:: TangemThemePreview { WalletScreen( state = data, - marketsEntryComponent = object : MarketsEntryComponent { - @Composable - override fun BottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - modifier: Modifier, - ) { - Text("Markets Content") - } + bottomSheetContent = { + Text("Markets Content") }, + bottomSheetHeaderHeightProvider = { 10.dp }, + onBottomSheetStateChange = {}, ) } } diff --git a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt index 458cbcaef2..bca8b45d6e 100644 --- a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt +++ b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt @@ -151,7 +151,7 @@ class YieldSupplyPromoBannerKeyConverterTest { fiatAmount = null, fiatRate = null, priceChange = null, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = if (isYieldActive) { YieldSupplyStatus( isActive = true, diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt index e4fde45099..b6844fd970 100644 --- a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt @@ -115,7 +115,9 @@ class DefaultPromoDeeplinkHandlerTest { Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success)) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Activated), @@ -143,7 +145,9 @@ class DefaultPromoDeeplinkHandlerTest { Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_invalid_code_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_invalid_code)) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.InvalidPromoCode), @@ -172,7 +176,9 @@ class DefaultPromoDeeplinkHandlerTest { Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_error_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_error)) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Failed), @@ -209,7 +215,9 @@ class DefaultPromoDeeplinkHandlerTest { Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address)) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress), @@ -248,7 +256,9 @@ class DefaultPromoDeeplinkHandlerTest { Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success)) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Activated), @@ -264,7 +274,9 @@ class DefaultPromoDeeplinkHandlerTest { expectedMessage = resourceReference(R.string.bitcoin_promo_invalid_code), ) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.InvalidPromoCode), @@ -280,7 +292,9 @@ class DefaultPromoDeeplinkHandlerTest { expectedMessage = resourceReference(R.string.bitcoin_promo_activation_error), ) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Failed), @@ -296,7 +310,9 @@ class DefaultPromoDeeplinkHandlerTest { expectedMessage = resourceReference(R.string.bitcoin_promo_no_address), ) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress), @@ -312,7 +328,9 @@ class DefaultPromoDeeplinkHandlerTest { expectedMessage = resourceReference(R.string.bitcoin_promo_already_activated), ) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.PromoCodeAlreadyUsed), @@ -425,7 +443,9 @@ class DefaultPromoDeeplinkHandlerTest { Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address)) - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress), @@ -539,7 +559,9 @@ class DefaultPromoDeeplinkHandlerTest { coVerify(exactly = 1) { activateBitcoinPromocodeUseCase.invoke("bc1qcard", promoCode) } coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke("bc1qcustom", promoCode) } - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Activated), @@ -594,7 +616,9 @@ class DefaultPromoDeeplinkHandlerTest { coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) } - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress), @@ -649,7 +673,9 @@ class DefaultPromoDeeplinkHandlerTest { coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) } - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress), @@ -700,7 +726,9 @@ class DefaultPromoDeeplinkHandlerTest { coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) } - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress), @@ -751,7 +779,9 @@ class DefaultPromoDeeplinkHandlerTest { coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) } - verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) } + verify( + exactly = 1, + ) { analyticsEventHandler.send(ofType()) } verify(exactly = 1) { analyticsEventHandler.send( PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress), diff --git a/features/walletconnect/impl/detekt-baseline-debug.xml b/features/walletconnect/impl/detekt-baseline-debug.xml index 569bdbc31c..017f85e30e 100644 --- a/features/walletconnect/impl/detekt-baseline-debug.xml +++ b/features/walletconnect/impl/detekt-baseline-debug.xml @@ -31,10 +31,6 @@ NamedArguments:WcSendTransactionModel.kt$WcSendTransactionModel$buildUiState(securityCheck, useCase, signState, isApprovalMethod) NestedScopeFunctions:WcSendAndReceiveBlockAidUiConverter.kt$WcSendAndReceiveBlockAidUiConverter$let { spendAllowanceUMConverter.convert( WcSpendAllowanceUMConverter.Input( approvedAmount = it, onLearnMoreClick = value.onApproveLearnMoreClick, ), ) } NoNameShadowing:WcNavigationUtils.kt$model - NonBooleanPropertyPrefixedWithIs:WcConnectionsModel.kt$WcConnectionsModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:WcPairModel.kt$WcPairModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase - NonBooleanPropertyPrefixedWithIs:WcPortfolioNameDelegate.kt$WcPortfolioNameDelegate$val isAccountMode = isAccountsModeEnabledUseCase.invoke() .stateIn(scope = scope, started = SharingStarted.Eagerly, initialValue = false) - NonBooleanPropertyPrefixedWithIs:WcRoutingModel.kt$WcRoutingModel$private val isSlotEmpty = MutableStateFlow(true) NullCheckOnMutableProperty:WcCommonTransactionComponentDelegate.kt$WcCommonTransactionComponentDelegate$if (contentStack != null) { val content by contentStack!!.subscribeAsState() BackHandler(onBack = ::onChildBack) content.active.instance.BottomSheet() } NullableBooleanCheck:WcPairModel.kt$WcPairModel$isAccountMode ?: false NullableToStringCall:WcEstimatedWalletChangeUMConverter.kt$WcEstimatedWalletChangeUMConverter$${value.sign} diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt index fc0d38f971..57298f9b5d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt @@ -68,7 +68,7 @@ internal class WcConnectionsModel @Inject constructor( val bottomSheetNavigation: SlotNavigation = SlotNavigation() init { - analytics.send(WcAnalyticEvents.ScreenOpened) + analytics.send(WcAnalyticEvents.ScreenOpened()) listenQrUpdates() if (accountsFeatureToggles.isFeatureEnabled) { listenWcSessions() diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index aaf93e1932..e0a0182370 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -31,6 +31,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.walletconnect.WcAnalyticAccountEvents import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcPairError.Unknown @@ -118,7 +119,7 @@ internal class WcPairModel @Inject constructor( init { if (accountsFeatureToggles.isFeatureEnabled) { portfolioFetcher = portfolioFetcherFactory.create( - mode = PortfolioFetcher.Mode.All(onlyMultiCurrency = true), + mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true), scope = modelScope, ) modelScope.launch { @@ -319,6 +320,17 @@ internal class WcPairModel @Inject constructor( val selectedPortfolio = selectedPortfolio.replayCache.firstOrNull() val wallet = selectedPortfolio?.first ?: selectedUserWalletFlow.value val account = selectedPortfolio?.second?.account + + modelScope.launch { + if (selectorController.isAccountModeSync() && account != null) { + val derivationIndex = when (account) { + is Account.CryptoPortfolio -> account.derivationIndex.value + } + analytics.send(WcAnalyticAccountEvents.PairButtonConnect(derivationIndex)) + } else { + analytics.send(WcAnalyticEvents.PairButtonConnect()) + } + } wcPairUseCase.approve( WcSessionApprove( wallet = wallet, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 2ed61e7552..611010e6f2 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -171,7 +171,7 @@ internal class WcSendTransactionModel @Inject constructor( feeReloadState.value = false modelScope.launch { feeSelectorReloadTrigger.triggerUpdate( - FeeSelectorData(removeSuggestedFee = feeStateConfiguration !is FeeStateConfiguration.Suggestion), + FeeSelectorData(isRemoveSuggestedFee = feeStateConfiguration !is FeeStateConfiguration.Suggestion), ) } } diff --git a/features/welcome/impl/detekt-baseline-debug.xml b/features/welcome/impl/detekt-baseline-debug.xml index eebd20e796..9c54bf2d00 100644 --- a/features/welcome/impl/detekt-baseline-debug.xml +++ b/features/welcome/impl/detekt-baseline-debug.xml @@ -4,10 +4,8 @@ BooleanPropertyNaming:WelcomeModel.kt$WelcomeModel$private var routedOut = false BooleanPropertyNaming:WelcomeUM.kt$WelcomeUM.SelectWallet$val showUnlockWithBiometricButton: Boolean = false - CanBeNonNullable:WelcomeModel.kt$WelcomeModel$specificWalletId: UserWalletId? MultilineLambdaItParameter:WelcomeModel.kt$WelcomeModel${ if (it.isEmpty()) { router.replaceAll(AppRoute.Home()) } wallets.value = it } MultilineLambdaItParameter:WelcomeModel.kt$WelcomeModel${ it.handle( specificWalletId = null, onUserCancelled = { tryToUnlockWithAccessCodeRightAway() }, ) setSelectWalletState() } - NullableToStringCall:WelcomeModel.kt$WelcomeModel$$specificWalletId ReusedModifierInstance:Welcome.kt$WelcomePlain(modifier = modifier) ReusedModifierInstance:Welcome.kt$WelcomeSelectWallet( state = st, modifier = modifier, ) diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index bf40c9be77..249109f0e4 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -3,6 +3,11 @@ package com.tangem.features.welcome.impl.model import com.tangem.common.routing.AppRoute import com.tangem.common.ui.userwallet.handle import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic +import com.tangem.core.analytics.models.event.SignIn +import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router @@ -36,6 +41,8 @@ internal class WelcomeModel @Inject constructor( private val nonBiometricUnlockWalletUseCase: NonBiometricUnlockWalletUseCase, private val canUseBiometryUseCase: CanUseBiometryUseCase, private val walletsRepository: WalletsRepository, + private val trackingContextProxy: TrackingContextProxy, + private val analyticsEventHandler: AnalyticsEventHandler, userWalletsFetcherFactory: UserWalletsFetcher.Factory, ) : Model() { @@ -51,6 +58,18 @@ internal class WelcomeModel @Inject constructor( modelScope.launch { val userWallets = userWalletsListRepository.userWalletsSync() val userWallet = userWallets.first { it.walletId == walletId } + trackingContextProxy.addContext(userWallet) + val signInType = when { + !userWallet.isLocked -> SignIn.ButtonWallet.SignInType.NoSecurity + userWallet is UserWallet.Cold -> SignIn.ButtonWallet.SignInType.Card + else -> SignIn.ButtonWallet.SignInType.AccessCode + } + analyticsEventHandler.send( + event = SignIn.ButtonWallet( + signInType = signInType, + walletsCount = userWallets.size, + ), + ) onUserWalletClick(userWallet) } }, @@ -64,6 +83,8 @@ internal class WelcomeModel @Inject constructor( userWalletsListRepository.load() wallets.value = walletsFetcher.userWallets.first() + analyticsEventHandler.send(SignIn.ScreenOpened(wallets.value.size)) + launch { walletsFetcher.userWallets .collectLatest { @@ -119,6 +140,7 @@ internal class WelcomeModel @Inject constructor( showUnlockWithBiometricButton = canUnlockWithBiometrics(), addWalletClick = ::addWalletClick, onUnlockWithBiometricClick = { + analyticsEventHandler.send(SignIn.ButtonUnlockAllWithBiometric()) modelScope.launch { userWalletsListRepository.unlockAllWallets() .onRight { @@ -140,6 +162,7 @@ internal class WelcomeModel @Inject constructor( } private fun addWalletClick() { + analyticsEventHandler.send(SignIn.ButtonAddWallet(AnalyticsParam.ScreensSources.SignIn)) router.push(AppRoute.CreateWalletSelection) } @@ -154,6 +177,7 @@ internal class WelcomeModel @Inject constructor( if (userWallet.isLocked.not()) { // If the wallet is not locked, we can proceed to the wallet screen directly userWalletsListRepository.select(userWallet.walletId) + trackSignInEvent(userWallet, Basic.SignedIn.SignInType.NoSecurity) router.replaceAll(AppRoute.Wallet) return@launch } @@ -190,6 +214,7 @@ internal class WelcomeModel @Inject constructor( router.replaceAll(AppRoute.Wallet) }, onUserCancelled = { onUserCancelled() }, + analyticsEventHandler = analyticsEventHandler, showMessage = uiMessageSender::send, ) } @@ -203,4 +228,15 @@ internal class WelcomeModel @Inject constructor( } } } + + private suspend fun trackSignInEvent(userWallet: UserWallet, type: Basic.SignedIn.SignInType) { + val walletsCount = userWalletsListRepository.userWalletsSync().size + trackingContextProxy.addContext(userWallet) + analyticsEventHandler.send( + event = Basic.SignedIn( + signInType = type, + walletsCount = walletsCount, + ), + ) + } } \ No newline at end of file diff --git a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt index 37b00d61b2..0b1c498d8d 100644 --- a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt +++ b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt @@ -189,7 +189,7 @@ sealed class YieldSupplyAnalytics( ), ) - data object ApyChartViewed : YieldSupplyAnalytics( + class ApyChartViewed : YieldSupplyAnalytics( event = "APY Chart", ) diff --git a/features/yield-supply/impl/detekt-baseline-debug.xml b/features/yield-supply/impl/detekt-baseline-debug.xml index 1063bc0890..703322ff19 100644 --- a/features/yield-supply/impl/detekt-baseline-debug.xml +++ b/features/yield-supply/impl/detekt-baseline-debug.xml @@ -3,7 +3,6 @@ BooleanPropertyNaming:YieldSupplyApyComponent.kt$YieldSupplyApyComponent$val state by loadingState.collectAsState() - BooleanPropertyNaming:YieldSupplyModel.kt$YieldSupplyModel$private val handleNavigation = params.handleNavigation BooleanPropertyNaming:YieldSupplyModel.kt$YieldSupplyModel$val processing = uiState.value is YieldSupplyUM.Processing BooleanPropertyNaming:YieldSupplyModel.kt$YieldSupplyModel$val showWarningIcon = !yieldSupplyStatus.isAllowedToSpend BooleanPropertyNaming:YieldSupplyUM.kt$YieldSupplyUM.Content$val showInfoIcon: Boolean @@ -16,10 +15,6 @@ NamedArguments:YieldSupplyActiveContent.kt$Icon( painterResource(R.drawable.ic_token_info_24), contentDescription = null, modifier = Modifier.size(20.dp), tint = TangemTheme.colors.text.warning, ) NamedArguments:YieldSupplyChartUM.kt$YieldSupplyMarketChartDataUM.Companion$YieldSupplyMarketChartDataUM(y = y, x = x, avr = 5.15, "%.1f") NoNameShadowing:YieldSupplyStopEarningModel.kt$YieldSupplyStopEarningModel$fee - NonBooleanPropertyPrefixedWithIs:YieldSupplyActiveComponent.kt$YieldSupplyActiveComponent.Params$val isBalanceHiddenFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:YieldSupplyActiveEntryComponent.kt$YieldSupplyActiveEntryComponent.Params$val isBalanceHiddenFlow: StateFlow<Boolean> - NonBooleanPropertyPrefixedWithIs:YieldSupplyActiveEntryModel.kt$YieldSupplyActiveEntryModel$val isTransactionInProgressFlow: StateFlow<Boolean> field = MutableStateFlow(false) - NonBooleanPropertyPrefixedWithIs:YieldSupplyModel.kt$YieldSupplyModel$val isBalanceHiddenFlow: StateFlow<Boolean> field = MutableStateFlow(false) NullableToStringCall:YieldSupplyModel.kt$YieldSupplyModel$$tokenPendingStatus NullableToStringCall:YieldSupplyModel.kt$YieldSupplyModel$$tokenProtocolStatus NullableToStringCall:YieldSupplyModel.kt$YieldSupplyModel$$yieldSupplyStatus diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/model/YieldSupplyPromoModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/model/YieldSupplyPromoModel.kt index 642777e14b..a1384942e1 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/model/YieldSupplyPromoModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/promo/model/YieldSupplyPromoModel.kt @@ -60,7 +60,7 @@ internal class YieldSupplyPromoModel @Inject constructor( } override fun onApyInfoClick() { - analytics.send(YieldSupplyAnalytics.ApyChartViewed) + analytics.send(YieldSupplyAnalytics.ApyChartViewed()) bottomSheetNavigation.activate(YieldSupplyPromoConfig.Apy) } diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 8c10bcaf9d..e5b0dbadd1 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1317" +tangemBlockchainSdk = "develop-1327" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-568" +tangemCardSdk = "develop-573" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt index 82780c4813..328bf4053e 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt @@ -28,7 +28,7 @@ class AccountNodeRecognizer(private val blockchain: Blockchain) { // region Ethereum-like blockchains blockchain == Blockchain.Tezos -> ACCOUNT_NODE_INDEX blockchain == Blockchain.Quai -> ACCOUNT_NODE_INDEX - blockchain.isEvm() -> ADDRESS_INDEX_NODE_INDEX + blockchain.isEvm() && derivationPath.isEthDerivation() -> ADDRESS_INDEX_NODE_INDEX // endregion blockchain.isUTXO -> ACCOUNT_NODE_INDEX else -> ACCOUNT_NODE_INDEX @@ -75,6 +75,13 @@ class AccountNodeRecognizer(private val blockchain: Blockchain) { .getOrNull() } + private fun DerivationPath.isEthDerivation(): Boolean { + fun getIndex(nodeIndex: Int) = nodes.getOrNull(nodeIndex)?.getIndex(false) + + return getIndex(FIRST_NODE_INDEX) == ETH_FIRST_NODE_VALUE && + getIndex(SECOND_NODE_INDEX) == ETH_SECOND_NODE_VALUE + } + @Suppress("LongMethod") private fun Blockchain.isAccountsSupported(): Boolean { return when (this) { @@ -253,5 +260,9 @@ class AccountNodeRecognizer(private val blockchain: Blockchain) { private companion object { const val ACCOUNT_NODE_INDEX = 2 const val ADDRESS_INDEX_NODE_INDEX = 4 + const val FIRST_NODE_INDEX = 0 + const val ETH_FIRST_NODE_VALUE = 44L + const val SECOND_NODE_INDEX = 1 + const val ETH_SECOND_NODE_VALUE = 60L } } \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/DetektConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/DetektConfigurations.kt index 264610cc2a..c6778122da 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/DetektConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/DetektConfigurations.kt @@ -5,6 +5,7 @@ import com.tangem.plugin.configuration.utils.findPlugin import io.gitlab.arturbosch.detekt.CONFIGURATION_DETEKT import io.gitlab.arturbosch.detekt.CONFIGURATION_DETEKT_PLUGINS import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.DetektCreateBaselineTask import io.gitlab.arturbosch.detekt.extensions.DetektExtension import org.gradle.api.Project import org.gradle.kotlin.dsl.configure @@ -28,6 +29,9 @@ private fun DetektExtension.configure(project: Project) { "plugins/detekt-rules/app-detekt-config.yml" ) ) + + ignoredBuildTypes = listOf("release", "internal", "external", "mocked") + ignoredFlavors = listOf("huawei") } private fun Project.configureDetektPlugins() { @@ -41,9 +45,14 @@ private fun Project.configureDetektPlugins() { } private fun Project.configureDetektTask() { - tasks.withType { - include("**/*.kt") - exclude("**/resources/**", "**/build/**") + tasks.withType().configureEach { + logger.lifecycle("[Detekt] Configuring task: $name in project: ${project.path}") + + source = fileTree(projectDir) { + include("src/main/**/*.kt") + exclude("**/build/**") + } + reports { sarif { required.set(false) @@ -55,4 +64,14 @@ private fun Project.configureDetektTask() { jvmTarget = "17" } + + tasks.withType().configureEach { + logger.lifecycle("[Detekt Baseline] Configuring task: $name in project: ${project.path}") + + source = fileTree(projectDir) { + include("src/main/**/*.kt") + exclude("**/build/**") + } + jvmTarget = "17" + } } \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt index 1ff0e4258f..50de4b0279 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt @@ -1,21 +1,29 @@ package com.tangem.plugin.configuration.configurations import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.dsl.kotlinExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile internal fun Project.configureKotlinCompilerOptions() { + extensions.configure { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + } + kotlinExtension.sourceSets.all { // https://github.com/Kotlin/KEEP/blob/explicit-backing-fields-re/proposals/explicit-backing-fields.md languageSettings.enableLanguageFeature("ExplicitBackingFields") } project.tasks.withType { - kotlinOptions { - jvmTarget = "17" - allWarningsAsErrors = false + compilerOptions { + allWarningsAsErrors.set(false) // this is required to produce a unique META-INF/*.kotlin_module files - moduleName = project.path.removePrefix(":").replace(':', '-') + freeCompilerArgs.add("-module-name=${project.path.removePrefix(":").replace(':', '-')}") } } } \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt index 7011f2d423..bb9c96653e 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt @@ -3,11 +3,8 @@ package com.tangem.plugin.configuration.configurations.extension import com.android.build.gradle.BaseExtension import com.tangem.plugin.configuration.model.AppConfig import com.tangem.plugin.configuration.utils.findPlugin -import com.tangem.plugin.configuration.utils.findVersion import org.gradle.api.JavaVersion import org.gradle.api.Project -import org.gradle.kotlin.dsl.apply -import org.gradle.kotlin.dsl.plugins internal fun BaseExtension.configureCompileSdk() { compileSdkVersion(AppConfig.compileSdkVersion) @@ -28,6 +25,7 @@ internal fun BaseExtension.configureCompose(project: Project) { contains(Regex(pattern = ":presentation\$")) || contains(Regex(pattern = ":app\$")) || // TODO: [REDACTED_JIRA] contains(Regex(pattern = ":features:markets:api\$")) || // provides Composable function + contains(Regex(pattern = ":features:feed:api\$")) || // provides Composable function contains(Regex(pattern = ":features:manage-tokens:api\$")) || // provides Composable function contains(Regex(pattern = ":features:txhistory:api\$")) || // provides Composable function contains(Regex(pattern = ":impl\$")) diff --git a/settings.gradle.kts b/settings.gradle.kts index 60632a9860..f6e74fac62 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -300,6 +300,9 @@ include(":features:yield-supply:impl") include(":features:feed:api") include(":features:feed:impl") + +include(":features:news:news-details:api") +include(":features:news:news-details:impl") // endregion Feature modules // region Domain modules diff --git a/tangem-android-tools b/tangem-android-tools index 3da4c865da..d7950d60bb 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 3da4c865da5f2a61d5170357c60de4ddd8425815 +Subproject commit d7950d60bb4c6353f2aa3364073c6ec72167c666 diff --git a/test/mock/detekt-baseline-main.xml b/test/mock/detekt-baseline-main.xml new file mode 100644 index 0000000000..dd89921096 --- /dev/null +++ b/test/mock/detekt-baseline-main.xml @@ -0,0 +1,7 @@ + + + + + MultilineLambdaItParameter:MockAccounts.kt$MockAccounts${ val account = createAccount(derivationIndex = it + 1, userWalletId = userWalletId) add(account) } + + diff --git a/test/mock/src/main/java/com/tangem/test/mock/MockAccounts.kt b/test/mock/src/main/java/com/tangem/test/mock/MockAccounts.kt index c8b8f57517..f275e2b0d2 100644 --- a/test/mock/src/main/java/com/tangem/test/mock/MockAccounts.kt +++ b/test/mock/src/main/java/com/tangem/test/mock/MockAccounts.kt @@ -19,12 +19,14 @@ object MockAccounts { fun createAccountList( activeAccounts: Int, totalAccounts: Int = activeAccounts, + totalArchivedAccounts: Int = 0, userWalletId: UserWalletId = this.userWalletId, ): AccountList { return AccountList( userWalletId = userWalletId, accounts = createAccounts(count = activeAccounts, userWalletId = userWalletId), totalAccounts = totalAccounts, + totalArchivedAccounts = totalArchivedAccounts, ).getOrNull()!! } diff --git a/update_detekt_baseline.sh b/update_detekt_baseline.sh index 1e6de62f27..b30b5d31f0 100755 --- a/update_detekt_baseline.sh +++ b/update_detekt_baseline.sh @@ -7,7 +7,7 @@ cd "$SCRIPT_DIR" OUTPUT_FILE="${1:-detekt_baseline_report.txt}" -INITIAL_ISSUES=1802 +INITIAL_ISSUES=1933 log() { echo "$@" | tee -a "$OUTPUT_FILE" @@ -24,7 +24,7 @@ log "" log "Step 1: Running detekt to check for new issues..." log "" -if ./gradlew detekt detektDebug detektGoogleDebug; then +if ./gradlew detekt detektMain; then log "✓ Detekt passed - no new issues found" log "" else @@ -35,10 +35,10 @@ else exit 1 fi -log "Step 2: Updating detekt baseline for debug variant..." +log "Step 2: Updating detekt baseline ..." log "" -./gradlew detektBaselineDebug +./gradlew detektBaselineMain log "" log "Baseline updated successfully!" @@ -54,11 +54,11 @@ module_count=0 temp_file=$(mktemp) -find . -name "detekt-baseline-debug.xml" -type f | while IFS= read -r file; do - issue_count=$(grep -c "" "$file" 2>/dev/null || echo "0") +find . \( -name "detekt-baseline-main.xml" -o -name "detekt-baseline-debug.xml" \) -type f | while IFS= read -r file; do + issue_count=$(grep -c "" "$file" 2>/dev/null) || issue_count=0 if [ "$issue_count" -gt 0 ]; then - module_name=$(echo "$file" | sed 's|^\./||' | sed 's|/detekt-baseline-debug.xml$||') + module_name=$(echo "$file" | sed 's|^\./||' | sed 's|/detekt-baseline-main.xml$||' | sed 's|/detekt-baseline-debug.xml$||') echo "$issue_count|$module_name" fi done | sort -rn -t'|' -k1 > "$temp_file"