diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6f915e67ee..efa92ac9ff 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -179,6 +179,7 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.decompose) implementation(projects.core.error.ext) + implementation(projects.core.security) implementation(projects.libs.crypto) implementation(projects.libs.auth) implementation(projects.libs.blockchainSdk) @@ -215,6 +216,7 @@ dependencies { implementation(projects.data.walletManager) implementation(projects.data.yieldSupply) implementation(projects.data.hotWallet) + implementation(projects.data.news) /** Features */ implementation(projects.features.referral.impl) @@ -352,7 +354,6 @@ dependencies { /** DI */ implementation(deps.hilt.android) - kapt(deps.hilt.kapt) kapt(deps.hilt.compilerx) @@ -384,10 +385,10 @@ dependencies { implementation(deps.prettyLogger) implementation(deps.decompose.ext.compose) implementation(deps.moshi.adapters) - implementation(deps.moshi.kotlin) ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) + implementation(files("libs/dexprotector-annotations.jar")) /** Testing libraries */ testImplementation(projects.common.test) @@ -417,7 +418,6 @@ dependencies { implementation(deps.camera.camera2) implementation(deps.camera.lifecycle) implementation(deps.camera.view) - implementation(deps.listenableFuture) implementation(deps.mlKit.barcodeScanning) diff --git a/app/libs/dexprotector-annotations.jar b/app/libs/dexprotector-annotations.jar new file mode 100644 index 0000000000..3f2f98f07f Binary files /dev/null and b/app/libs/dexprotector-annotations.jar differ 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..5b5902f3cc 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,12 @@ 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 + is SignIn.ErrorBiometricUpdated -> !event.isFromUnlockAll 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..e24d5594e5 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,23 @@ 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 + is SignIn.ErrorBiometricUpdated -> !event.isFromUnlockAll + 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/core/security/DefaultDeviceSecurityInfoProvider.kt b/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt new file mode 100644 index 0000000000..7463e33236 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.core.security + +import com.dexprotector.rtc.RtcStatus +import com.tangem.security.DeviceSecurityInfoProvider + +internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider { + override val isRooted: Boolean + get() = RtcStatus.getRtcStatus().root + override val isBootloaderUnlocked: Boolean + get() = RtcStatus.getRtcStatus().unlockedBootloader + override val isXposed: Boolean + get() = RtcStatus.getRtcStatus().xposed +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt index 4f7f4a0166..80e06699c1 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -115,6 +115,7 @@ internal class DefaultTangemPayStorage @Inject constructor( appPreferencesStore.store(PreferencesKeys.getTangemPayCustomerWalletAddressKey(userWalletId), "") appPreferencesStore.store(PreferencesKeys.getTangemPayOrderIdKey(customerWalletAddress), "") appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false) + appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), false) } override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, orderId: String) { @@ -144,6 +145,20 @@ internal class DefaultTangemPayStorage @Inject constructor( } } + override suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) { + withContext(dispatcherProvider.io) { + appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide) + } + } + + override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean { + return withContext(dispatcherProvider.io) { + appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), + ) == true + } + } + private fun createAuthTokensKey(address: String): String = "${AUTH_TOKENS_DEFAULT_KEY}_$address" private fun createWithdrawOrderIdKey(userWalletId: UserWalletId): String = "${WITHDRAW_ORDER_ID_KEY}_$userWalletId" diff --git a/app/src/main/java/com/tangem/tap/di/core/security/SecurityModule.kt b/app/src/main/java/com/tangem/tap/di/core/security/SecurityModule.kt new file mode 100644 index 0000000000..b4e64ccb37 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/core/security/SecurityModule.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.di.core.security + +import com.tangem.security.DeviceSecurityInfoProvider +import com.tangem.tap.core.security.DefaultDeviceSecurityInfoProvider +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 SecurityModule { + + @Provides + @Singleton + fun provideDeviceSecurityInfoProvider(): DeviceSecurityInfoProvider { + return DefaultDeviceSecurityInfoProvider() + } +} \ No newline at end of file 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..6e154f3fc1 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 @@ -8,10 +8,10 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.promo.PromoRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier -import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher +import com.tangem.domain.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 @@ -36,6 +36,14 @@ object MarketsDomainModule { return GetMarketsTokenListFlowUseCase(marketsTokenRepository = marketsTokenRepository) } + @Provides + @Singleton + fun provideGetTopFiveMarketTokenUseCase( + marketsTokenRepository: MarketsTokenRepository, + ): GetTopFiveMarketTokenUseCase { + return GetTopFiveMarketTokenUseCase(marketsTokenRepository = marketsTokenRepository) + } + @Provides @Singleton fun provideGetTokenPriceChartUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenPriceChartUseCase { @@ -65,20 +73,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), ) @@ -118,13 +128,11 @@ object MarketsDomainModule { @Provides @Singleton - fun provideGetStakingNotificationMaxApyUseCase( - settingsRepository: SettingsRepository, + fun provideShouldShowYieldModeMarketPromoUseCase( promoRepository: PromoRepository, marketsTokenRepository: MarketsTokenRepository, - ): GetStakingNotificationMaxApyUseCase { - return GetStakingNotificationMaxApyUseCase( - settingsRepository = settingsRepository, + ): ShouldShowYieldModeMarketPromoUseCase { + return ShouldShowYieldModeMarketPromoUseCase( promoRepository = promoRepository, marketsTokenRepository = marketsTokenRepository, ) 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/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index aea1890763..08a4bc7a3b 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 @@ -2,7 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.staking.* import com.tangem.domain.staking.repositories.* -import com.tangem.domain.staking.single.SingleYieldBalanceFetcher +import com.tangem.domain.staking.single.SingleStakingBalanceFetcher import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import dagger.Module @@ -107,11 +107,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/di/domain/YieldSupplyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt index b8c7e6dfc7..3301b9c21a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt @@ -1,10 +1,10 @@ package com.tangem.tap.di.domain import com.tangem.domain.blockaid.BlockAidGasEstimate -import com.tangem.domain.transaction.FeeRepository -import com.tangem.domain.transaction.error.FeeErrorResolver import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.transaction.FeeRepository +import com.tangem.domain.transaction.error.FeeErrorResolver import com.tangem.domain.yield.supply.YieldSupplyErrorResolver import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository @@ -16,6 +16,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +@Suppress("TooManyFunctions") @Module @InstallIn(SingletonComponent::class) internal object YieldSupplyDomainModule { @@ -225,4 +226,12 @@ internal object YieldSupplyDomainModule { fun provideYieldSupplyGetDustMinAmountUseCase(): YieldSupplyGetDustMinAmountUseCase { return YieldSupplyGetDustMinAmountUseCase() } + + @Provides + @Singleton + fun provideYieldSupplyGetAvailabilityUseCase( + yieldSupplyRepository: YieldSupplyRepository, + ): YieldSupplyGetAvailabilityUseCase { + return YieldSupplyGetAvailabilityUseCase(yieldSupplyRepository) + } } \ No newline at end of file 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/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt index 5c813c1a93..bc05e53d63 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPayGenerateAddressAndSignChallengeTask.kt @@ -11,7 +11,7 @@ import com.tangem.common.core.TangemSdkError import com.tangem.core.error.ext.tangemError import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.card.common.visa.VisaUtilities -import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource +import com.tangem.domain.visa.datasource.TangemPayRemoteDataSource import com.tangem.domain.visa.error.VisaActivationError import com.tangem.domain.visa.model.TangemPayInitialCredentials import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet @@ -31,7 +31,7 @@ import kotlinx.coroutines.withContext class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor( @Assisted private val coroutineScope: CoroutineScope, private val dispatchersProvider: CoroutineDispatcherProvider, - private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource, + private val tangemPayRemoteDataSource: TangemPayRemoteDataSource, ) : CardSessionRunnable { override fun run(session: CardSession, callback: CompletionCallback) { @@ -52,7 +52,7 @@ class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor( val userWalletId = UserWalletIdBuilder.walletPublicKey(wallet.publicKey) val challenge = withContext(dispatchersProvider.io) { - visaAuthRemoteDataSource.getCustomerWalletAuthChallenge( + tangemPayRemoteDataSource.getCustomerWalletAuthChallenge( customerWalletAddress = address, customerWalletId = userWalletId.stringValue, ) @@ -71,7 +71,7 @@ class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor( } val authTokens = withContext(dispatchersProvider.io) { - visaAuthRemoteDataSource.getTokenWithCustomerWallet( + tangemPayRemoteDataSource.getTokenWithCustomerWallet( sessionId = challenge.session.sessionId, signature = signedData.signature, nonce = signedData.dataToSign.hashToSign, 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/root/RootDetectedWarningComponent.kt b/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningComponent.kt new file mode 100644 index 0000000000..16077bce72 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningComponent.kt @@ -0,0 +1,68 @@ +package com.tangem.tap.features.root + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.essenty.instancekeeper.getOrCreateSimple +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.components.DialogFullScreen +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.security.DeviceSecurityInfoProvider +import com.tangem.security.isSecurityExposed +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +@Suppress("UnusedPrivateProperty") +class RootDetectedWarningComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: Unit, + private val securityInfoProvider: DeviceSecurityInfoProvider, + private val settingsRepository: SettingsRepository, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val isShown = instanceKeeper.getOrCreateSimple { MutableStateFlow(false) } + + suspend fun tryToShowWarningAndWaitContinuation() { + if (isShown.value) return + + if (settingsRepository.isRootDetectedWarningShown().not() && securityInfoProvider.isSecurityExposed()) { + isShown.value = true + } + + isShown.first { it == false } // Wait until the warning is dismissed + } + + @Composable + override fun Content(modifier: Modifier) { + val isShownState by isShown.collectAsStateWithLifecycle() + + if (isShownState) { + DialogFullScreen(onDismissRequest = {}) { + RootDetectedWarningContent( + modifier = modifier, + onContinueClick = remember(this) { ::onContinueClick }, + ) + } + } + } + + private fun onContinueClick() { + componentScope.launch { + settingsRepository.setRootDetectedWarningShown(true) + isShown.value = false + } + } + + @AssistedFactory + interface Factory : ComponentFactory { + override fun create(context: AppComponentContext, params: Unit): RootDetectedWarningComponent + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningContent.kt b/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningContent.kt new file mode 100644 index 0000000000..9dc01d8a13 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningContent.kt @@ -0,0 +1,93 @@ +package com.tangem.tap.features.root + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +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 +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.icons.HighlightedIcon +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.wallet.R + +@Composable +internal fun RootDetectedWarningContent(modifier: Modifier = Modifier, onContinueClick: () -> Unit = {}) { + Column( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary) + .statusBarsPadding() + .padding(horizontal = 16.dp), + ) { + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.Center, + ) { + InfoBlock( + modifier = Modifier.padding(top = 48.dp, bottom = 24.dp), + ) + } + + PrimaryButton( + modifier = Modifier + .navigationBarsPadding() + .padding(bottom = 16.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.common_understand_continue), + onClick = onContinueClick, + ) + } +} + +@Composable +private fun InfoBlock(modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + HighlightedIcon( + icon = R.drawable.ic_alert_circle_24, + iconTint = TangemTheme.colors.icon.warning, + ) + + SpacerH(20.dp) + + Text( + text = stringResourceSafe(R.string.root_detected_warning_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + + SpacerH(12.dp) + + Text( + modifier = Modifier.padding(horizontal = 24.dp), + text = stringResourceSafe(R.string.root_detected_warning_description), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + } +} + +@Preview +@Composable +private fun Preview() { + TangemThemePreview { + RootDetectedWarningContent() + } +} \ No newline at end of file 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 9664e2bb7f..fcfcc02601 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 @@ -45,7 +45,6 @@ internal class WelcomeModel @Inject constructor( init { subscribeToStoreChanges() initGlobalState() - analyticsEventsHandler.send(SignIn.ScreenOpened()) val welcomeAction = when (params.launchMode) { is InitScreenLaunchMode.WithCardScan -> WelcomeAction.ProceedWithCard 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/RootContent.kt b/app/src/main/java/com/tangem/tap/routing/RootContent.kt index add9354694..c21f7d2617 100644 --- a/app/src/main/java/com/tangem/tap/routing/RootContent.kt +++ b/app/src/main/java/com/tangem/tap/routing/RootContent.kt @@ -47,6 +47,7 @@ internal fun RootContent( modifier: Modifier = Modifier, wcContent: @Composable (modifier: Modifier) -> Unit, hotAccessCodeContent: @Composable (modifier: Modifier) -> Unit, + rootDetectedWarningContent: @Composable (modifier: Modifier) -> Unit, ) { val context = LocalContext.current @@ -82,6 +83,8 @@ internal fun RootContent( hotAccessCodeContent(Modifier.fillMaxSize()) + rootDetectedWarningContent(Modifier.fillMaxSize()) + TangemSnackbarHost( modifier = Modifier .align(Alignment.BottomCenter) 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..23bc630cec 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 @@ -34,6 +38,7 @@ import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.hot.TangemHotSDKProxy import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog +import com.tangem.tap.features.root.RootDetectedWarningComponent import com.tangem.tap.routing.RootContent import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.component.RoutingComponent.Child @@ -60,9 +65,13 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private val tangemHotSDKProxy: TangemHotSDKProxy, private val hotAccessCodeRequestComponentFactory: HotAccessCodeRequestComponent.Factory, private val hotAccessCodeRequesterProxy: HotWalletPasswordRequesterProxy, + private val rootDetectedWarningComponentFactory: RootDetectedWarningComponent.Factory, 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, @@ -78,6 +87,11 @@ internal class DefaultRoutingComponent @AssistedInject constructor( .create(child("hotAccessCodeRequestComponent"), Unit) } + private val rootDetectedWarningComponent: RootDetectedWarningComponent by lazy { + rootDetectedWarningComponentFactory + .create(child("rootDetectedWarningComponent"), Unit) + } + private val navigation = navigationProvider.getOrCreateTyped() private val stack: Value> = childStack( @@ -127,6 +141,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private fun initializeInitialNavigation() { if (initialStack.isNullOrEmpty()) { componentScope.launch { + rootDetectedWarningComponent.tryToShowWarningAndWaitContinuation() val initialRoute = resolveInitialRoute() router.replaceAll(initialRoute) } @@ -151,6 +166,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( ) } else -> { + trackSignInEvent() AppRoute.Wallet } }.also { @@ -169,6 +185,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( modifier = modifier, wcContent = { wcRoutingComponent.Content(it) }, hotAccessCodeContent = { hotAccessCodeRequestComponent.Content(it) }, + rootDetectedWarningContent = { rootDetectedWarningComponent.Content(it) }, ) } @@ -235,4 +252,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..715bf51463 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 @@ -40,6 +40,8 @@ import com.tangem.features.tangempay.components.TangemPayDetailsContainerCompone import com.tangem.features.tangempay.components.TangemPayOnboardingComponent import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.ContinueOnboarding import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.Deeplink +import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.FromBannerOnMain +import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.FromBannerInSettings import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.wallet.WalletEntryComponent import com.tangem.features.walletconnect.components.WalletConnectEntryComponent @@ -529,7 +531,9 @@ internal class ChildFactory @Inject constructor( is AppRoute.CreateMobileWallet -> { createComponentChild( context = context, - params = Unit, + params = CreateMobileWalletComponent.Params( + source = route.source, + ), componentFactory = createMobileWalletComponentFactory, ) } @@ -565,7 +569,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, ), @@ -665,6 +669,9 @@ internal class ChildFactory @Inject constructor( ) is AppRoute.TangemPayOnboarding.Mode.Deeplink -> Deeplink( deeplink = mode.deeplink, + ) + is AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings -> FromBannerInSettings + is AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain -> FromBannerOnMain( userWalletId = mode.userWalletId, ) }, 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..c2d8b480d7 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 @@ -431,13 +433,20 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class Deeplink( val deeplink: String, - val userWalletId: UserWalletId?, ) : Mode() @Serializable data class ContinueOnboarding( - val userWalletId: UserWalletId?, + val userWalletId: UserWalletId, ) : Mode() + + @Serializable + data class FromBannerOnMain( + val userWalletId: UserWalletId, + ) : Mode() + + @Serializable + data object FromBannerInSettings : Mode() } } 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-markets/.gitignore b/common/ui-markets/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/common/ui-markets/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/common/ui-markets/build.gradle.kts b/common/ui-markets/build.gradle.kts new file mode 100644 index 0000000000..b83e4298e2 --- /dev/null +++ b/common/ui-markets/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.common.ui.markets" +} + +dependencies { + /** Project - Core */ + implementation(projects.core.ui) + implementation(projects.core.utils) + + /** Project - Common */ + implementation(projects.common.uiCharts) + implementation(projects.common.ui) + + /** Project - Domain */ + implementation(projects.domain.models) + + implementation(deps.lifecycle.compose) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.ui.utils) + implementation(deps.kotlin.immutable.collections) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/components/MarketListItem.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItem.kt similarity index 97% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/components/MarketListItem.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItem.kt index 9f011aa99a..f2931ff828 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/components/MarketListItem.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItem.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.ui.market.components +package com.tangem.common.ui.markets import android.content.res.Configuration import androidx.compose.foundation.background @@ -19,6 +19,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import com.tangem.common.ui.charts.MarketChartMini import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.common.ui.markets.preview.MarketChartListItemPreviewDataProvider import com.tangem.common.ui.tokens.TokenPriceText import com.tangem.core.ui.R import com.tangem.core.ui.components.* @@ -32,13 +34,11 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.MarketsTestTags import com.tangem.core.ui.windowsize.WindowSizeType -import com.tangem.features.feed.ui.market.preview.MarketChartListItemPreviewDataProvider -import com.tangem.features.feed.ui.market.state.MarketsListItemUM import com.tangem.utils.StringsSigns.MINUS import kotlin.random.Random @Composable -internal fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) { +fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) { MarketsListItemContent( modifier = modifier .fillMaxWidth() diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/components/MarketsListItemPlaceholder.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketsListItemPlaceholder.kt similarity index 98% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/components/MarketsListItemPlaceholder.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketsListItemPlaceholder.kt index ab52d344f0..3e11c3c039 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/components/MarketsListItemPlaceholder.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketsListItemPlaceholder.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.ui.market.components +package com.tangem.common.ui.markets import android.content.res.Configuration import androidx.compose.foundation.background diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListItemUM.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/models/MarketsListItemUM.kt similarity index 96% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListItemUM.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/models/MarketsListItemUM.kt index 5e518373dc..9d48452aef 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListItemUM.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/models/MarketsListItemUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.ui.market.state +package com.tangem.common.ui.markets.models import androidx.compose.runtime.Immutable import com.tangem.common.ui.charts.state.MarketChartLook diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/preview/MarketChartListItemPreviewDataProvider.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/preview/MarketChartListItemPreviewDataProvider.kt similarity index 95% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/preview/MarketChartListItemPreviewDataProvider.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/preview/MarketChartListItemPreviewDataProvider.kt index 98dd938ce2..7cac4e8314 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/preview/MarketChartListItemPreviewDataProvider.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/preview/MarketChartListItemPreviewDataProvider.kt @@ -1,15 +1,15 @@ -package com.tangem.features.feed.ui.market.preview +package com.tangem.common.ui.markets.preview import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.features.feed.ui.market.state.MarketsListItemUM import kotlinx.collections.immutable.persistentListOf @Suppress("MagicNumber") -internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider( +class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider( collection = listOf( MarketsListItemUM( id = CryptoCurrency.RawID("1"), 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..91e26bb895 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,48 @@ 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.resolveReference 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) @@ -81,7 +88,7 @@ private fun TrendingArticle(articleConfigUM: ArticleConfigUM) { ArticleInfo( score = articleConfigUM.score, - createdAt = articleConfigUM.createdAt, + createdAt = articleConfigUM.createdAt.resolveReference(), ) SpacerH(32.dp) @@ -98,7 +105,7 @@ private fun DefaultArticle(articleConfigUM: ArticleConfigUM) { Column(modifier = Modifier.padding(12.dp)) { ArticleInfo( score = articleConfigUM.score, - createdAt = articleConfigUM.createdAt, + createdAt = articleConfigUM.createdAt.resolveReference(), ) SpacerH(8.dp) @@ -121,54 +128,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 +145,7 @@ private fun Tags(tags: ImmutableList, modifier: Modifier = Modifie maxLines = 1, overflow = expandIndicator, ) { index -> - ArticleBadge(articleTagUM = tags[index]) + Label(state = tags[index]) } } @@ -189,14 +155,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,19 +172,18 @@ 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( id = 1, title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)", score = 9.5f, - createdAt = "1h ago", + createdAt = TextReference.Str("1h ago"), isTrending = true, tags = tags, isViewed = false, 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..46714a0ba8 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,13 +1,15 @@ package com.tangem.common.ui.news +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableSet data class ArticleConfigUM( val id: Int, val title: String, val score: Float, - val createdAt: String, + val createdAt: TextReference, 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 029babd400..dd92464d2a 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 @@ -20,7 +20,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.StakingAvailability import com.tangem.domain.staking.model.StakingOption import com.tangem.domain.staking.model.stakekit.Yield @@ -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( @@ -277,14 +277,18 @@ class TokenItemStateConverter( val stakingAvailability = stakingApyMap[currencyStatus.currency] as? StakingAvailability.Available ?: 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 = when (val stakingOptions = stakingAvailability.option) { - is StakingOption.P2P -> null // todo p2p - is StakingOption.StakeKit -> if (hasStakedBalance) { + is StakingOption.P2P -> { + // P2P or no balance: use preferred validators + // TODO add p2p logic + null + } + is StakingOption.StakeKit -> if (stakeKitBalance != null) { val validatorsByAddress = stakingOptions.yield.validators.associateBy { it.address } - yieldBalance.balance.items + stakeKitBalance.balance.items .mapNotNull { it.validatorAddress } .firstNotNullOfOrNull { address -> validatorsByAddress[address]?.rewardInfo @@ -306,7 +310,7 @@ class TokenItemStateConverter( return StakingLocalInfo( rate = rateInfo?.rate, - isActive = hasStakedBalance, + isActive = stakeKitBalance != null, // todo add p2p check rewardType = rateInfo?.type, ) } 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..41fdd9e856 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,8 @@ import com.tangem.domain.common.wallets.error.UnlockWalletError.UnableToUnlock.R inline fun UnlockWalletError.handle( onAlreadyUnlocked: () -> Unit = {}, onUserCancelled: () -> Unit = {}, + analyticsEventHandler: AnalyticsEventHandler, + isFromUnlockAll: Boolean, noinline showMessage: (EventMessage) -> Unit, ) { when (this) { @@ -30,15 +34,26 @@ 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( + isFromUnlockAll = isFromUnlockAll, + error = this, + analyticsEventHandler = analyticsEventHandler, + showDialog = showMessage, + ) } } -fun handleUnableToUnlock(error: UnlockWalletError.UnableToUnlock, showDialog: (DialogMessage) -> Unit) { +fun handleUnableToUnlock( + isFromUnlockAll: Boolean, + 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(isFromUnlockAll)) 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..f62e70074f --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SignIn.kt @@ -0,0 +1,54 @@ +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") + + data class ErrorBiometricUpdated( + val isFromUnlockAll: Boolean, + ) : 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 963f847260..dce534df8d 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/TangemPay.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt index 30a3300678..c0a5c07775 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt @@ -31,7 +31,7 @@ internal class TangemPay( private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.DEV, - baseUrl = "https://api.dev.us.paera.com/bff/", + baseUrl = "https://api.dev.us.paera.com/bff-v2/", headers = createHeaders(), ) @@ -43,7 +43,7 @@ internal class TangemPay( private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.PROD, - baseUrl = "https://api.us.paera.com/bff/", + baseUrl = "https://api.us.paera.com/bff-v2/", headers = createHeaders(), ) 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/markets/models/response/TokenMarketListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt index 3c1829900b..31168b8c5b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt @@ -26,6 +26,7 @@ data class TokenMarketListResponse( @Json(name = "market_cap") val marketCap: BigDecimal?, @Json(name = "is_under_market_cap_limit") val isUnderMarketCapLimit: Boolean?, @Json(name = "staking_opportunities") val stakingOpportunities: List?, + @Json(name = "max_yield_apy") val maxYieldApy: BigDecimal?, ) { @JsonClass(generateAdapter = true) 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/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index 9c4fca8149..07719ff953 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -3,109 +3,13 @@ package com.tangem.datasource.api.pay import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.pay.models.request.* import com.tangem.datasource.api.pay.models.response.* -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.Header -import retrofit2.http.POST -import retrofit2.http.PUT -import retrofit2.http.Path -import retrofit2.http.Query +import retrofit2.http.* private const val TX_HISTORY_PAGING_DEFAULT_LIMIT = 20 @Suppress("TooManyFunctions") interface TangemPayApi { - // region: auth - - @POST("v1/auth/challenge") - suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): ApiResponse - - @POST("v1/auth/challenge") - suspend fun generateNonceByCardWallet( - @Body request: GenerateNoneByCardWalletRequest, - ): ApiResponse - - @POST("v1/auth/token") - suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse - - @POST("v1/auth/token") - suspend fun getTokenByCustomerWallet(@Body request: GetTokenByCustomerWalletRequest): ApiResponse - - @POST("v1/auth/token/refresh") - suspend fun refreshCustomerWalletAccessToken( - @Body request: RefreshCustomerWalletAccessTokenRequest, - ): ApiResponse - - @POST("v1/auth/token") - suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse - - @POST("v1/auth/token/refresh") - suspend fun refreshCardIdAccessToken(@Body request: RefreshTokenByCardIdRequest): ApiResponse - - @POST("v1/auth/token/refresh") - suspend fun refreshCardWalletAccessToken(@Body request: RefreshTokenByCardWalletRequest): ApiResponse - - @POST("v1/auth/token/exchange") - suspend fun exchangeAccessToken(@Body request: ExchangeAccessTokenRequest): ApiResponse - - // endregion - - // region: activation - - @POST("v1/activation/status") - suspend fun getRemoteActivationStatus( - @Header("Authorization") authHeader: String, - @Body request: ActivationStatusRequest, - ): ApiResponse - - @POST("v1/activation/acceptance/message") - suspend fun getCardWalletAcceptance( - @Header("Authorization") authHeader: String, - @Body request: GetCardWalletAcceptanceRequest, - ): ApiResponse - - @POST("v1/activation/acceptance/message") - suspend fun getCustomerWalletAcceptance( - @Header("Authorization") authHeader: String, - @Body request: GetCustomerWalletAcceptanceRequest, - ): ApiResponse - - @POST("v1/activation/data") - suspend fun activateByCardWallet( - @Header("Authorization") authHeader: String, - @Body body: ActivationByCardWalletRequest, - ): ApiResponse - - @POST("v1/activation/data") - suspend fun activateByCustomerWallet( - @Header("Authorization") authHeader: String, - @Body body: ActivationByCustomerWalletRequest, - ): ApiResponse - - @POST("v1/activation/pin") - suspend fun setPinCode( - @Header("Authorization") authHeader: String, - @Body body: SetPinCodeRequest, - ): ApiResponse - - // endregion - - @GET("customer/info") - suspend fun getCustomerInfo( - @Header("Authorization") authHeader: String, - @Query("card_id") cardId: String, - ): ApiResponse - - @GET("product_instance/transactions") - suspend fun getTxHistory( - @Header("Authorization") authHeader: String, - @Query("customer_id") customerId: String, - @Query("product_instance_id") productInstanceId: String, - @Query("limit") limit: Int, - @Query("offset") offset: Int, - ): ApiResponse - @GET("v1/customer/transactions") suspend fun getTangemPayTxHistory( @Header("Authorization") authHeader: String, @@ -128,6 +32,9 @@ interface TangemPayApi { @POST("v1/deeplink/validate") suspend fun validateDeeplink(@Body body: DeeplinkValidityRequest): ApiResponse + @GET("v1/customer/eligibility") + suspend fun checkCustomerEligibility(): ApiResponse + @GET("v1/order/{order_id}") suspend fun getOrder( @Header("Authorization") authHeader: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt index 11f891c668..b926f0fa05 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt @@ -5,5 +5,10 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class CheckCustomerWalletResponse( - @Json(name = "id") val id: String?, -) \ No newline at end of file + @Json(name = "result") val result: Result?, + @Json(name = "error") val error: String?, +) { + data class Result( + @Json(name = "id") val id: String?, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerEligibilityResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerEligibilityResponse.kt new file mode 100644 index 0000000000..8ddcfab3f7 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerEligibilityResponse.kt @@ -0,0 +1,15 @@ +package com.tangem.datasource.api.pay.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class CustomerEligibilityResponse( + @Json(name = "result") val result: Result?, + @Json(name = "error") val error: String?, +) { + @JsonClass(generateAdapter = true) + data class Result( + @Json(name = "is_tangem_pay_available") val isTangemPayAvailable: Boolean, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt index fd89823bea..baf6d5d265 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt @@ -12,11 +12,11 @@ data class CustomerMeResponse( data class Result( @Json(name = "id") val id: String, @Json(name = "state") val state: String, - @Json(name = "createdAt") val createdAt: String, + @Json(name = "created_at") val createdAt: String, @Json(name = "product_instance") val productInstance: ProductInstance?, @Json(name = "payment_account") val paymentAccount: PaymentAccount?, @Json(name = "kyc") val kyc: Kyc?, - @Json(name = "depositAddress") val depositAddress: String?, + @Json(name = "deposit_address") val depositAddress: String?, @Json(name = "card") val card: Card?, @Json(name = "balance") val balance: BalanceResponse?, ) @@ -24,49 +24,49 @@ data class CustomerMeResponse( @JsonClass(generateAdapter = true) data class ProductInstance( @Json(name = "id") val id: String, - @Json(name = "cid") val cid: String, + @Json(name = "cid") val cid: String?, @Json(name = "card_id") val cardId: String, - @Json(name = "card_wallet_address") val cardWalletAddress: String, + @Json(name = "card_wallet_address") val cardWalletAddress: String?, @Json(name = "status") val status: Status, @Json(name = "updated_at") val updatedAt: String, @Json(name = "payment_account_id") val paymentAccountId: String, ) { @JsonClass(generateAdapter = false) enum class Status { - @Json(name = "new") + @Json(name = "NEW") NEW, - @Json(name = "ready_for_manufacturing") + @Json(name = "READY_FOR_MANUFACTURING") READY_FOR_MANUFACTURING, - @Json(name = "manufacturing") + @Json(name = "MANUFACTURING") MANUFACTURING, - @Json(name = "sent_to_delivery") + @Json(name = "SENT_TO_DELIVERY") SENT_TO_DELIVERY, - @Json(name = "delivered") + @Json(name = "DELIVERED") DELIVERED, - @Json(name = "activating") + @Json(name = "ACTIVATING") ACTIVATING, - @Json(name = "active") + @Json(name = "ACTIVE") ACTIVE, - @Json(name = "blocked") + @Json(name = "BLOCKED") BLOCKED, - @Json(name = "deactivating") + @Json(name = "DEACTIVATING") DEACTIVATING, - @Json(name = "deactivated") + @Json(name = "DEACTIVATED") DEACTIVATED, - @Json(name = "canceled") + @Json(name = "CANCELED") CANCELED, - @Json(name = "unknown") + @Json(name = "UNKNOWN") UNKNOWN, } } @@ -91,8 +91,8 @@ data class CustomerMeResponse( @JsonClass(generateAdapter = true) data class Card( @Json(name = "token") val token: String, - @Json(name = "expiration_month") val expirationMonth: Int, - @Json(name = "expiration_year") val expirationYear: Int, + @Json(name = "expiration_month") val expirationMonth: String, + @Json(name = "expiration_year") val expirationYear: String, @Json(name = "emboss_name") val embossName: String, @Json(name = "card_type") val cardType: String, @Json(name = "card_status") val cardStatus: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayErrorResponse.kt similarity index 90% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaErrorResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayErrorResponse.kt index b211405000..7ae65cd653 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaErrorResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayErrorResponse.kt @@ -4,7 +4,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class VisaErrorResponse( +data class TangemPayErrorResponse( @Json(name = "error") val error: Error, ) { @JsonClass(generateAdapter = true) 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/api/visa/VisaApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/VisaApi.kt new file mode 100644 index 0000000000..cda093a821 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/VisaApi.kt @@ -0,0 +1,104 @@ +package com.tangem.datasource.api.visa + +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardIdRequest +import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest +import com.tangem.datasource.api.pay.models.request.SetPinCodeRequest +import com.tangem.datasource.api.visa.models.request.ActivationByCardWalletRequest +import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest +import com.tangem.datasource.api.visa.models.request.ActivationStatusRequest +import com.tangem.datasource.api.visa.models.request.ExchangeAccessTokenRequest +import com.tangem.datasource.api.visa.models.request.GenerateNoneByCardIdRequest +import com.tangem.datasource.api.visa.models.request.GenerateNoneByCardWalletRequest +import com.tangem.datasource.api.visa.models.request.GetAccessTokenByCardIdRequest +import com.tangem.datasource.api.visa.models.request.GetAccessTokenByCardWalletRequest +import com.tangem.datasource.api.visa.models.request.GetCardWalletAcceptanceRequest +import com.tangem.datasource.api.visa.models.request.GetCustomerWalletAcceptanceRequest +import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse +import com.tangem.datasource.api.visa.models.response.GenerateNonceResponse +import com.tangem.datasource.api.visa.models.response.JWTResponse +import com.tangem.datasource.api.visa.models.response.VisaCustomerInfo +import com.tangem.datasource.api.visa.models.response.VisaDataToSignResponse +import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.Query + +interface VisaApi { + + @POST("v1/auth/token/refresh") + suspend fun refreshCardWalletAccessToken(@Body request: RefreshTokenByCardWalletRequest): ApiResponse + + @POST("v1/auth/challenge") + suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): ApiResponse + + @POST("v1/auth/challenge") + suspend fun generateNonceByCardWallet( + @Body request: GenerateNoneByCardWalletRequest, + ): ApiResponse + + @POST("v1/auth/token") + suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse + + @POST("v1/auth/token") + suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse + + @POST("v1/auth/token/refresh") + suspend fun refreshCardIdAccessToken(@Body request: RefreshTokenByCardIdRequest): ApiResponse + + @POST("v1/auth/token/exchange") + suspend fun exchangeAccessToken(@Body request: ExchangeAccessTokenRequest): ApiResponse + + @POST("v1/activation/status") + suspend fun getRemoteActivationStatus( + @Header("Authorization") authHeader: String, + @Body request: ActivationStatusRequest, + ): ApiResponse + + @POST("v1/activation/acceptance/message") + suspend fun getCardWalletAcceptance( + @Header("Authorization") authHeader: String, + @Body request: GetCardWalletAcceptanceRequest, + ): ApiResponse + + @POST("v1/activation/acceptance/message") + suspend fun getCustomerWalletAcceptance( + @Header("Authorization") authHeader: String, + @Body request: GetCustomerWalletAcceptanceRequest, + ): ApiResponse + + @POST("v1/activation/data") + suspend fun activateByCardWallet( + @Header("Authorization") authHeader: String, + @Body body: ActivationByCardWalletRequest, + ): ApiResponse + + @POST("v1/activation/data") + suspend fun activateByCustomerWallet( + @Header("Authorization") authHeader: String, + @Body body: ActivationByCustomerWalletRequest, + ): ApiResponse + + @POST("v1/activation/pin") + suspend fun setPinCode( + @Header("Authorization") authHeader: String, + @Body body: SetPinCodeRequest, + ): ApiResponse + + @GET("customer/info") + suspend fun getCustomerInfo( + @Header("Authorization") authHeader: String, + @Query("card_id") cardId: String, + ): ApiResponse + + @GET("product_instance/transactions") + suspend fun getTxHistory( + @Header("Authorization") authHeader: String, + @Query("customer_id") customerId: String, + @Query("product_instance_id") productInstanceId: String, + @Query("limit") limit: Int, + @Query("offset") offset: Int, + ): ApiResponse +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCardWalletRequest.kt similarity index 95% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationByCardWalletRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCardWalletRequest.kt index a5dafd7b32..c16198f0c6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationByCardWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCardWalletRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationByCustomerWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCustomerWalletRequest.kt similarity index 90% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationByCustomerWalletRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCustomerWalletRequest.kt index 731188d92a..ace2090603 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationByCustomerWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCustomerWalletRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationStatusRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationStatusRequest.kt similarity index 82% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationStatusRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationStatusRequest.kt index 9f3e017e55..7ce0614abc 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ActivationStatusRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationStatusRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ExchangeAccessTokenRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ExchangeAccessTokenRequest.kt similarity index 82% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ExchangeAccessTokenRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ExchangeAccessTokenRequest.kt index 164d71fa5c..63b9fe43d8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/ExchangeAccessTokenRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ExchangeAccessTokenRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNoneByCardIdRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardIdRequest.kt similarity index 85% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNoneByCardIdRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardIdRequest.kt index 7ab945b8f8..d3aa5eaa50 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNoneByCardIdRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardIdRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNoneByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt similarity index 86% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNoneByCardWalletRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt index 95d2f8f9bf..69c2f7a7d5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNoneByCardWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetAccessTokenByCardIdRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardIdRequest.kt similarity index 86% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetAccessTokenByCardIdRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardIdRequest.kt index 380284bab7..c212c81412 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetAccessTokenByCardIdRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardIdRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetAccessTokenByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt similarity index 87% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetAccessTokenByCardWalletRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt index 50fc683c9b..df5d0c3935 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetAccessTokenByCardWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetCardWalletAcceptanceRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCardWalletAcceptanceRequest.kt similarity index 86% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetCardWalletAcceptanceRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCardWalletAcceptanceRequest.kt index de4cec941b..dd084fa326 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetCardWalletAcceptanceRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCardWalletAcceptanceRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetCustomerWalletAcceptanceRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCustomerWalletAcceptanceRequest.kt similarity index 87% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetCustomerWalletAcceptanceRequest.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCustomerWalletAcceptanceRequest.kt index 7ff5c16337..3948fc43b3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetCustomerWalletAcceptanceRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCustomerWalletAcceptanceRequest.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.request +package com.tangem.datasource.api.visa.models.request import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardActivationRemoteStateResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt similarity index 93% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardActivationRemoteStateResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt index ae9c701b06..a34a31a366 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardActivationRemoteStateResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardActivationRemoteStateResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.response +package com.tangem.datasource.api.visa.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/GenerateNonceResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt similarity index 86% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/GenerateNonceResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt index 958580bc5e..9a79a457b8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/GenerateNonceResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.response +package com.tangem.datasource.api.visa.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/JWTResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt similarity index 93% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/JWTResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt index c28a882ae9..0380074d02 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/JWTResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.response +package com.tangem.datasource.api.visa.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaCustomerInfo.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaCustomerInfo.kt similarity index 89% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaCustomerInfo.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaCustomerInfo.kt index b6a227f62f..70d5f6ac94 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaCustomerInfo.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaCustomerInfo.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.response +package com.tangem.datasource.api.visa.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaDataToSignResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaDataToSignResponse.kt similarity index 84% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaDataToSignResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaDataToSignResponse.kt index a35c99ab59..a19d3e7ca4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaDataToSignResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaDataToSignResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.response +package com.tangem.datasource.api.visa.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaTxHistoryResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaTxHistoryResponse.kt similarity index 98% rename from core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaTxHistoryResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaTxHistoryResponse.kt index aea17cc4c1..7c4d7d0a22 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/VisaTxHistoryResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaTxHistoryResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.pay.models.response +package com.tangem.datasource.api.visa.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass 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/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 6c07212416..511a2dab3d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -5,7 +5,6 @@ import com.tangem.datasource.api.common.blockaid.BlockAidApi import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfigs -import com.tangem.datasource.api.common.config.MoonPay import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.config.managers.DevApiConfigsManager import com.tangem.datasource.api.common.config.managers.MockApiConfigsManager @@ -21,6 +20,7 @@ import com.tangem.datasource.api.pay.TangemPayAuthApi import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.YieldSupplyApi +import com.tangem.datasource.api.visa.VisaApi import com.tangem.datasource.di.utils.RetrofitApiBuilder import com.tangem.datasource.di.utils.RetrofitApiBuilder.Timeouts import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -129,7 +129,16 @@ internal object NetworkModule { @Provides @Singleton - fun provideTangemVisaApi(retrofitApiBuilder: RetrofitApiBuilder): TangemPayApi { + fun provideTangemPayApi(retrofitApiBuilder: RetrofitApiBuilder): TangemPayApi { + return retrofitApiBuilder.build( + apiConfigId = ApiConfig.ID.TangemPay, + applyTimeoutAnnotations = false, + ) + } + + @Provides + @Singleton + fun provideVisaApi(retrofitApiBuilder: RetrofitApiBuilder): VisaApi { return retrofitApiBuilder.build( apiConfigId = ApiConfig.ID.TangemPay, applyTimeoutAnnotations = false, 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/news/trending/DefaultTrendingNewsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/news/trending/DefaultTrendingNewsStore.kt index 23c1cad1db..157a5430e3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/news/trending/DefaultTrendingNewsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/news/trending/DefaultTrendingNewsStore.kt @@ -1,25 +1,25 @@ package com.tangem.datasource.local.news.trending import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.domain.models.news.ShortArticle +import com.tangem.domain.models.news.TrendingNews import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -private typealias TrendingCache = Map> +private typealias TrendingCache = Map internal class DefaultTrendingNewsStore( private val store: RuntimeSharedStore, ) : TrendingNewsStore { - override fun get(key: String): Flow> { - return store.get().map { it[key].orEmpty() } + override fun get(key: String): Flow { + return store.get().map { it[key] ?: TrendingNews.Data(emptyList()) } } - override suspend fun getSyncOrNull(key: String): List? { + override suspend fun getSyncOrNull(key: String): TrendingNews? { return store.getSyncOrNull()?.get(key) } - override suspend fun store(key: String, value: List) { + override suspend fun store(key: String, value: TrendingNews) { store.update(emptyMap()) { current -> current + (key to value) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/news/trending/TrendingNewsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/news/trending/TrendingNewsStore.kt index 24afaabda0..643ad55ee6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/news/trending/TrendingNewsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/news/trending/TrendingNewsStore.kt @@ -1,15 +1,15 @@ package com.tangem.datasource.local.news.trending -import com.tangem.domain.models.news.ShortArticle +import com.tangem.domain.models.news.TrendingNews import kotlinx.coroutines.flow.Flow interface TrendingNewsStore { - fun get(key: String): Flow> + fun get(key: String): Flow - suspend fun getSyncOrNull(key: String): List? + suspend fun getSyncOrNull(key: String): TrendingNews? - suspend fun store(key: String, value: List) + suspend fun store(key: String, value: TrendingNews) suspend fun clear() } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 037f71a2a6..78e99e42bd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -27,6 +27,8 @@ object PreferencesKeys { val SAVE_USER_WALLETS_KEY by lazy { booleanPreferencesKey(name = "saveUserWallets") } + val ROOT_DETECTED_WARNING_SHOWN_KEY by lazy { booleanPreferencesKey(name = "rootDetectedWarningShown") } + val SHOULD_SHOW_ASK_BIOMETRY_KEY by lazy { booleanPreferencesKey("saveUserWalletShown") } val APP_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "launchCount") } @@ -77,8 +79,8 @@ object PreferencesKeys { val SHOULD_SHOW_MARKETS_TOOLTIP_KEY by lazy { booleanPreferencesKey(name = "shouldShowMarketsTooltip") } - val MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY by lazy { - booleanPreferencesKey(name = "marketsStakingNotificationHideClicked") + val MARKETS_YIELD_SUPPLY_NOTIFICATION_HIDE_CLICKED_KEY by lazy { + booleanPreferencesKey(name = "marketsYieldSupplyNotificationHideClicked") } val WALLET_FIRST_USAGE_DATE_KEY by lazy { longPreferencesKey(name = "walletFirstUsageDate") } @@ -108,6 +110,8 @@ object PreferencesKeys { val ONRAMP_TRANSACTIONS_STATUSES_KEY by lazy { stringPreferencesKey(name = "onrampTransactionsStatuses") } + val ONRAMP_HANDLED_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "onrampHandledTransactions") } + val ONBOARDING_FINALIZE_SCAN_RESPONSE_KEY by lazy { stringPreferencesKey(name = "onboardingFinalizeScanResponse") } val IS_GOOGLE_SERVICES_AVAILABLE_KEY by lazy { booleanPreferencesKey(name = "isGoogleServicesAvailable") } @@ -192,6 +196,9 @@ object PreferencesKeys { fun getTangemPayCheckCustomerByWalletId(userWalletId: UserWalletId) = booleanPreferencesKey("tangem_pay_check_customer_by_wallet_id_$userWalletId") + fun getTangemPayHideOnboardingKey(userWalletId: UserWalletId) = + booleanPreferencesKey("tangem_pay_hide_onboarding_key_$userWalletId") + // endregion } 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/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt index e32fe05975..a9279e9193 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt @@ -30,5 +30,9 @@ interface TangemPayStorage { suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) + suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean + + suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) + suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) } \ No newline at end of file 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..e176d0b768 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 @@ -250,7 +251,7 @@ internal class ProdApiConfigsManagerTest { id = ApiConfig.ID.TangemPay, expected = ApiEnvironmentConfig( environment = ApiEnvironment.DEV, - baseUrl = "https://api.dev.us.paera.com/bff/", + baseUrl = "https://api.dev.us.paera.com/bff-v2/", headers = mapOf( "version" to ProviderSuspend { VERSION_NAME }, "platform" to ProviderSuspend { "Android" }, @@ -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/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 0d5eaa197f..03ce372d0f 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -2017,7 +2017,7 @@ %1$s выведено из Aave Режим доходности инициализирован Режим доходности реактивирован - Перевод средств в Aave + Перевод в Aave %1$s отправлено в Aave Вывод из Aave Автоматически diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index d365c6e247..07f6bd26c8 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -103,10 +103,10 @@ Go to settings to enable biometric authentication in the Tangem App Enable biometric authentication Disabling %1$s will require you to enter your passcode to unlock the app and to interact with your wallet. - You’ll be asked for your wallet’s access code later so we can securely store it for future use - This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. + You\'ll be asked for your access code later for secure storage. + This will delete all saved wallet access codes. You\'ll need to enter the access code again to use the wallet. Removing the saved devices deletes all the saved wallets and their access codes from the app. - This will delete all the saved wallet access codes. Any further interaction with the wallet will require submitting the access code. + This will delete all saved wallet access codes. You’ll need to enter the access code again to use the wallet. Require Access Code This option turns off biometrics for sensitive actions. You’ll need to enter your access code each time you sign a transaction. Save Access Code @@ -145,10 +145,10 @@ Balances are hidden According to the blockchain developers, Kaspa tokens are currently in beta. Stay tuned for updates! Beta Mode - Biometrics are turned off on your device, so you can’t use them to unlock your wallets. Enable biometrics in your device settings to use this method again. + Biometrics are turned off on your device, so you can\'t use them to unlock your wallets. Enable biometrics in your device settings to use this method again. Biometric authentication disabled Please scan the card or ring - You’ve reached the limit of biometric attempts. Please unlock your wallet with a device tap or enter your access code. + You\'ve reached the limit of biometric attempts. Please unlock your wallet with a card/ring or enter your access code. Biometric authentication locked Please try again in 30 seconds or scan the card or ring Biometric login is temporarily locked. Please try again in 30 seconds, or unlock your wallet with a device tap or access code. @@ -182,7 +182,7 @@ Upgrade again Reset complete We recommend completing the reset process for all Tangem devices in this wallet. - You haven’t reset all your Tangem devices + Some Tangem devices still need to be reset. Disable this option if you don\'t want this card to be used to reset access codes on other cards or rings in this wallet. Please note that this will also prevent you from resetting the access code on this card. Allows you to use this card to reset access code on other cards in this wallet Access code recovery @@ -384,8 +384,9 @@ Transaction status Transactions Transfer - Unable to load the data… + Unable to load data… I understand + I understand, continue There was an error. Please try again. Unreachable Unstake @@ -575,7 +576,7 @@ Create New Wallet Order Tangem Scan Tangem - Do you want to allow “Tangem” to use biometric authentication? To confirm your identity and open the app + Do you want to allow \"Tangem\" to use biometric authentication to confirm your identity and open the app? to %s On %s network Are you sure you want to cancel access code setup? @@ -799,7 +800,7 @@ Add tokens Power up your assets while supplying them with instant access. %s Activate Yield Mode - You must update to %1$s in order to create mobile wallet + You must update to %1$s before creating a mobile wallet Mobile Wallet requires %1$s or later All news @@ -1090,6 +1091,8 @@ Please reset the next device to continue Ring owners get 3 commission-free swaps on Changelly until 15.11! Swap With 0% Fees Now! + Devices with root access are considered less secure. Your data may be exposed to additional risks. + Root access detected Log into the app and check your balance without scanning the card or ring Access the app Allow to use biometrics @@ -1426,6 +1429,7 @@ Your card is frozen. Get Help Other + Unable to use on rooted devices Completed Declined Pending @@ -1598,7 +1602,7 @@ Would you like to use\nPush-notifications? Enable push notifications to receive alerts when funds arrive in your wallet. Don\'t Miss a Transaction - Add new wallet + Add Wallet If you delete this wallet without a backup, you will permanently lose access to your funds. Are you sure you want to forget this wallet? An error has occurred, please scan your card or ring to log in @@ -1759,7 +1763,7 @@ Change access code Stay notified on wallet incoming transactions and Tangem updates. Push notifications may currently not work on Huawei devices. We\'re actively working on a solution and will release a fix in an upcoming update. Thank you for your understanding! - Transaction Notifications + Transaction notifications Set access code Wallet settings Tangem @@ -1952,7 +1956,7 @@ Suspicious transaction Already have Tangem Wallet? Thousands of assets - Best in class hardware wallet + Top-tier hardware wallet Fast delivery Start in one tap Seamless and secure @@ -1961,7 +1965,7 @@ Create or import a software wallet Create or import a software wallet on your phone. Start with Mobile Wallet - Other method + Other methods Use a Tangem hardware wallet Learn more & buy Discard diff --git a/core/security/.gitignore b/core/security/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/core/security/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/security/build.gradle.kts b/core/security/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/core/security/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} \ No newline at end of file diff --git a/core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt b/core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt new file mode 100644 index 0000000000..09d16c0a94 --- /dev/null +++ b/core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt @@ -0,0 +1,9 @@ +package com.tangem.security + +interface DeviceSecurityInfoProvider { + val isRooted: Boolean + val isBootloaderUnlocked: Boolean + val isXposed: Boolean +} + +fun DeviceSecurityInfoProvider.isSecurityExposed(): Boolean = isRooted || isBootloaderUnlocked || isXposed \ No newline at end of file diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 3f46d7909d..33e81eb02f 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -66,4 +66,6 @@ dependencies { testImplementation(deps.test.junit) testImplementation(deps.test.mockk) testImplementation(deps.test.truth) + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) } \ No newline at end of file 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/core/ui/src/main/java/com/tangem/core/ui/components/DialogFullScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/components/DialogFullScreen.kt index 6f70db0ba1..44e48c0fd0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/DialogFullScreen.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/DialogFullScreen.kt @@ -37,44 +37,46 @@ fun DialogFullScreen( decorFitsSystemWindows = false, ), content = { - val activityWindow = getActivityWindow() - val dialogWindow = getDialogWindow() - val parentView = LocalView.current.parent as View - SideEffect { - if (activityWindow != null && dialogWindow != null) { - val attributes = WindowManager.LayoutParams().apply { - copyFrom(activityWindow.attributes) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { - softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE - } else { - flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS - } - type = dialogWindow.attributes.type - } - - dialogWindow.attributes = attributes - parentView.layoutParams = - FrameLayout.LayoutParams( - activityWindow.decorView.width, - activityWindow.decorView.height, - ) - } - } - - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { - val systemUiController = rememberSystemUiController(getActivityWindow()) - val dialogSystemUiController = rememberSystemUiController(getDialogWindow()) - + ProvideSystemBarsIconsController { + val activityWindow = getActivityWindow() + val dialogWindow = getDialogWindow() + val parentView = LocalView.current.parent as View SideEffect { - systemUiController.setSystemBarsColor(color = Color.Transparent) - dialogSystemUiController.setSystemBarsColor(color = Color.Transparent) + if (activityWindow != null && dialogWindow != null) { + val attributes = WindowManager.LayoutParams().apply { + copyFrom(activityWindow.attributes) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + } else { + flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + } + type = dialogWindow.attributes.type + } + + dialogWindow.attributes = attributes + parentView.layoutParams = + FrameLayout.LayoutParams( + activityWindow.decorView.width, + activityWindow.decorView.height, + ) + } } - } - SystemBarsIconsDisposable(darkIcons = LocalIsInDarkTheme.current.not()) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + val systemUiController = rememberSystemUiController(getActivityWindow()) + val dialogSystemUiController = rememberSystemUiController(getDialogWindow()) - Surface(modifier = Modifier.fillMaxSize(), color = Color.Transparent) { - content() + SideEffect { + systemUiController.setSystemBarsColor(color = Color.Transparent) + dialogSystemUiController.setSystemBarsColor(color = Color.Transparent) + } + } + + SystemBarsIconsDisposable(darkIcons = LocalIsInDarkTheme.current.not()) + + Surface(modifier = Modifier.fillMaxSize(), color = Color.Transparent) { + content() + } } }, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt index f9baae94a7..faeb20ff2f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt @@ -308,13 +308,14 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) { } } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun DialogButtons( confirmButton: DialogButtonUM, dismissButton: DialogButtonUM?, modifier: Modifier = Modifier, ) { - Row( + FlowRow( modifier = modifier, horizontalArrangement = Arrangement.spacedBy( space = TangemTheme.dimens.spacing4, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt index f31c547290..cb3f2c538d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt @@ -2,17 +2,13 @@ package com.tangem.core.ui.components.bottomsheets.message import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon 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.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -25,6 +21,7 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTi import com.tangem.core.ui.components.buttons.common.TangemButton import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.components.icons.HighlightedIcon import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -144,20 +141,11 @@ private fun BottomSheetIcon(icon: MessageBottomSheetUMV2.Icon, modifier: Modifie MessageBottomSheetUMV2.Icon.BackgroundType.Warning -> TangemTheme.colors.icon.warning } - Box( - modifier = modifier - .size(TangemTheme.dimens.size56) - .clip(CircleShape) - .background(backgroundColor.copy(alpha = 0.1F)), - contentAlignment = Alignment.Center, - content = { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size32), - painter = painterResource(icon.res), - contentDescription = null, - tint = tint, - ) - }, + HighlightedIcon( + modifier = modifier, + icon = icon.res, + iconTint = tint, + backgroundColor = backgroundColor, ) } 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/fields/PinTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt index c348c0933f..9ee27e68ec 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt @@ -39,8 +39,8 @@ fun PinTextField( pinTextColor: PinTextColor, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, + focusRequester: FocusRequester = remember { FocusRequester() }, ) { - val focusRequester = remember { FocusRequester() } val textFieldValue = remember(value) { TextFieldValue(value, selection = TextRange(value.length)) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/icons/HighlightedIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/icons/HighlightedIcon.kt new file mode 100644 index 0000000000..a94578a5ff --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/icons/HighlightedIcon.kt @@ -0,0 +1,39 @@ +package com.tangem.core.ui.components.icons + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +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.res.painterResource +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun HighlightedIcon( + @DrawableRes icon: Int, + iconTint: Color, + modifier: Modifier = Modifier, + backgroundColor: Color = iconTint, +) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size56) + .clip(CircleShape) + .background(backgroundColor.copy(alpha = 0.1F)), + contentAlignment = Alignment.Center, + content = { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size32), + painter = painterResource(icon), + contentDescription = null, + tint = iconTint, + ) + }, + ) +} \ No newline at end of file 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..728da8c24c 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,7 +1,12 @@ 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.DateTimeZone import org.joda.time.format.DateTimeFormat import org.joda.time.format.DateTimeFormatter import org.joda.time.format.DateTimeFormatterBuilder @@ -80,6 +85,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" */ @@ -87,6 +99,23 @@ object DateTimeFormatters { getBestFormatterBySkeleton("dd.MM.yyyy HH:mm") } + /** + * Local full date formatter (e.g., "dd MMMM, HH:mm") + */ + val localFullDate: DateTimeFormatter by lazy { + DateTimeFormatterBuilder() + .appendDayOfMonth(2) + .appendLiteral(' ') + .appendMonthOfYearText() + .appendLiteral(", ") + .appendHourOfDay(2) + .appendLiteral(':') + .appendMinuteOfHour(2) + .toFormatter() + .withLocale(Locale.getDefault()) + .withZone(DateTimeZone.getDefault()) + } + fun formatDate(date: DateTime, formatter: DateTimeFormatter = dateFormatter): String { return formatter.print(date) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt index e01b50dfb3..9354511464 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt @@ -37,4 +37,51 @@ fun Long.toTimeFormat(formatter: DateTimeFormatter = DateTimeFormatters.timeForm */ fun Long.formatAsDateTime(formatter: DateTimeFormatter): String { return DateTimeFormatters.formatDate(date = DateTime(this, DateTimeZone.getDefault()), formatter = formatter) +} + +/** + * Parses an ISO 8601 date string and compares it to the current UTC time. + * + + * @param now The current date to compare against. + * @return A [FormattedDate] subclass. + */ +@Suppress("MagicNumber") +fun getFormattedDate(createdAt: String, now: DateTime): FormattedDate { + val pastDateUtc = try { + DateTime.parse(createdAt) + } catch (_: Exception) { + return FormattedDate.FullDate(createdAt) + } + + val pastDateLocal = pastDateUtc.withZone(DateTimeZone.getDefault()) + val isToday = pastDateLocal.isToday() + + val diffInMillis = now.millis - pastDateUtc.millis + val diffInMinutes = diffInMillis / (1000 * 60) + val diffInHours = diffInMillis / (1000 * 60 * 60) + + return when { + diffInMinutes < 1 -> FormattedDate.MinutesAgo(1) + diffInMinutes < 60 -> FormattedDate.MinutesAgo(diffInMinutes.toInt()) + diffInHours < 12 && isToday -> FormattedDate.HoursAgo(diffInHours.toInt()) + isToday -> { + val timeString = DateTimeFormatters.timeFormatter.print(pastDateLocal) + FormattedDate.Today(timeString) + } + else -> { + val dateString = DateTimeFormatters.localFullDate.print(pastDateLocal) + FormattedDate.FullDate(dateString) + } + } +} + +/** + * Representing different formatted date representations. + */ +sealed class FormattedDate { + data class MinutesAgo(val minutes: Int) : FormattedDate() + data class HoursAgo(val hours: Int) : FormattedDate() + data class Today(val time: String) : FormattedDate() + data class FullDate(val date: String) : FormattedDate() } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_analytics_up_mini_24.xml b/core/ui/src/main/res/drawable/ic_analytics_up_mini_24.xml new file mode 100644 index 0000000000..b7e8d62dff --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_analytics_up_mini_24.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + 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/ui/src/main/res/drawable/ic_tangem_pay_24.xml b/core/ui/src/main/res/drawable/ic_tangem_pay_24.xml new file mode 100644 index 0000000000..42130a709f --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tangem_pay_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/img_tangem_pay_onboarding_banner.webp b/core/ui/src/main/res/drawable/img_tangem_pay_onboarding_banner.webp new file mode 100644 index 0000000000..e3a0bccac9 Binary files /dev/null and b/core/ui/src/main/res/drawable/img_tangem_pay_onboarding_banner.webp differ diff --git a/core/ui/src/main/res/drawable/img_yield_supply_in_market_notification.webp b/core/ui/src/main/res/drawable/img_yield_supply_in_market_notification.webp new file mode 100644 index 0000000000..6f7bfc38ce Binary files /dev/null and b/core/ui/src/main/res/drawable/img_yield_supply_in_market_notification.webp differ diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateUtilsTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateUtilsTest.kt new file mode 100644 index 0000000000..6e145b2371 --- /dev/null +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateUtilsTest.kt @@ -0,0 +1,155 @@ +package com.tangem.core.ui.utils + +import com.google.common.truth.Truth +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DateUtilsTest { + + private lateinit var defaultTimeZone: DateTimeZone + + private val now = createDateTime(day = 14, hour = 12) + + @BeforeEach + fun setUp() { + defaultTimeZone = DateTimeZone.getDefault() + DateTimeZone.setDefault(DateTimeZone.forID("Europe/Moscow")) + } + + @AfterEach + fun tearDown() { + DateTimeZone.setDefault(defaultTimeZone) + } + + @Test + fun `should return MinutesAgo when difference is less than 1 minute`() { + val pastDate = now.minusSeconds(30) + val createdAt = pastDate.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java) + Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(1) + } + + @Test + fun `should return MinutesAgo when difference is 1 minute`() { + val pastDate = now.minusMinutes(1) + val createdAt = pastDate.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java) + Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(1) + } + + @Test + fun `should return MinutesAgo when difference is 30 minutes`() { + val pastDate = now.minusMinutes(30) + val createdAt = pastDate.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java) + Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(30) + } + + @Test + fun `should return MinutesAgo when difference is 59 minutes`() { + val pastDate = now.minusMinutes(59).minusSeconds(59) + val createdAt = pastDate.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java) + Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(59) + } + + @Test + fun `should return HoursAgo when difference is exactly 1 hour`() { + val pastDate = now.minusHours(1) + val createdAt = pastDate.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.HoursAgo::class.java) + Truth.assertThat((result as FormattedDate.HoursAgo).hours).isEqualTo(1) + } + + @Test + fun `should return HoursAgo when difference is 11 hours`() { + val pastDate = now.minusHours(11) + val createdAt = pastDate.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.HoursAgo::class.java) + Truth.assertThat((result as FormattedDate.HoursAgo).hours).isEqualTo(11) + } + + @Test + fun `should return Today when difference is exactly 12 hours`() { + val pastDate = createDateTime(day = 14, hour = 0) + val createdAt = pastDate.toString() + val nowInTest = createDateTime(day = 14, hour = 12) + val result = getFormattedDate(createdAt, nowInTest) + Truth.assertThat(result).isInstanceOf(FormattedDate.Today::class.java) + Truth.assertThat((result as FormattedDate.Today).time).isEqualTo("03:00") + } + + @Test + fun `should return FullDate when difference is 18 hours and past date is another day`() { + val pastDate = createDateTime(day = 13, hour = 18) + val createdAt = pastDate.toString() + val nowInTest = createDateTime(day = 14, hour = 12) + val result = getFormattedDate(createdAt, nowInTest) + Truth.assertThat(result).isInstanceOf(FormattedDate.FullDate::class.java) + } + + @Test + fun `should return FullDate when difference is exactly 24 hours`() { + val pastDate = now.minusDays(1) + val createdAt = pastDate.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.FullDate::class.java) + } + + @Test + fun `should return FullDate when difference is 2 days`() { + val pastDate = now.minusDays(2) + val createdAt = pastDate.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.FullDate::class.java) + } + + @Test + fun `should return FullDate with original string when format has wrong date separator`() { + val wrongFormat = "2025/10/14T12:00:00.000Z" + val result = getFormattedDate(wrongFormat, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.FullDate::class.java) + Truth.assertThat((result as FormattedDate.FullDate).date).isEqualTo(wrongFormat) + } + + @Test + fun `should handle future dates correctly`() { + val futureDate = now.plusHours(1) + val createdAt = futureDate.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java) + Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(1) + } + + @Test + fun `should handle edge case of exactly 0 milliseconds difference`() { + val createdAt = now.toString() + val result = getFormattedDate(createdAt, now) + Truth.assertThat(result).isInstanceOf(FormattedDate.MinutesAgo::class.java) + Truth.assertThat((result as FormattedDate.MinutesAgo).minutes).isEqualTo(1) + } + + private fun createDateTime(day: Int, hour: Int): DateTime { + return DateTime( + /* year = */ 2025, + /* monthOfYear = */ 10, + /* dayOfMonth = */ day, + /* hourOfDay = */ hour, + /* minuteOfHour = */ 0, + /* secondOfMinute = */ 0, + /* millisOfSecond = */ 0, + /* zone = */ DateTimeZone.UTC, + ) + } +} \ No newline at end of file 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/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 9dcc66efb0..2f974d4f31 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -20,6 +20,7 @@ import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.markets.models.response.TokenMarketExchangesResponse import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.models.account.DerivationIndex @@ -28,9 +29,8 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.pagination.* import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext -import java.math.BigDecimal +import timber.log.Timber import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong @@ -43,7 +43,6 @@ internal class DefaultMarketsTokenRepository( private val analyticsEventHandler: AnalyticsEventHandler, private val cacheRegistry: CacheRegistry, private val tokenExchangesStore: RuntimeStateStore>, - private val maxApyStore: RuntimeStateStore, private val networkFactory: NetworkFactory, excludedBlockchains: ExcludedBlockchains, ) : MarketsTokenRepository { @@ -95,8 +94,6 @@ internal class DefaultMarketsTokenRepository( val tokenMarketListWithMaxApy = TokenMarketListConverter.convert(res) - maxApyStore.store(tokenMarketListWithMaxApy.maxApy) - return BatchFetchResult.Success( data = tokenMarketListWithMaxApy.tokens, last = last, @@ -294,8 +291,24 @@ internal class DefaultMarketsTokenRepository( } } - override suspend fun getMaxApy(): Flow { - return maxApyStore.get() + override suspend fun showYieldModePromo( + appCurrency: AppCurrency, + interval: TokenMarketListConfig.Interval, + ): Boolean = try { + val hasYieldSupplyTokens = marketsApi.getCoinsList( + currency = appCurrency.code, + interval = interval.toRequestParam(), + order = TokenMarketListConfig.Order.YieldSupply.toRequestParam(), + offset = 0, + limit = 40, + timestamp = null, + search = null, + ).getOrThrow().tokens.isNotEmpty() + + hasYieldSupplyTokens + } catch (error: Exception) { + Timber.e(error) + false } inline fun catchListErrorAndSendEvent(block: () -> T): T { diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt index e280e160e1..7dce907fe8 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt @@ -16,6 +16,7 @@ internal fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) TokenMarketListConfig.Order.TopGainers -> "gainers" TokenMarketListConfig.Order.TopLosers -> "losers" TokenMarketListConfig.Order.Staking -> "staking" + TokenMarketListConfig.Order.YieldSupply -> "yield" } internal fun PriceChangeInterval.toRequestParam(): String = when (this) { diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt index 067f886293..ed6cb59dc4 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -40,7 +40,7 @@ internal object TokenMarketListConverter : Converter { - return fetchAndStoreTrendingNews(limit = limit, language = language) - } - - override fun observeTrendingNews(): Flow> { - return trendingNewsStore.get(TRENDING_NEWS_KEY) - } - - override suspend fun refreshTrendingNews(limit: Int, language: String?) { + override suspend fun fetchTrendingNews(limit: Int, language: String?) { fetchAndStoreTrendingNews(limit = limit, language = language) } + override fun observeTrendingNews(): Flow { + return trendingNewsStore.get(TRENDING_NEWS_KEY) + } + override suspend fun updateTrendingNewsViewed(articleIds: Collection, viewed: Boolean) { if (articleIds.isEmpty()) return - val current = trendingNewsStore.getSyncOrNull(TRENDING_NEWS_KEY).orEmpty() - if (current.isEmpty()) return + val currentResult = trendingNewsStore.getSyncOrNull(TRENDING_NEWS_KEY) ?: return + val currentArticles = when (currentResult) { + is TrendingNews.Data -> currentResult.articles + is TrendingNews.Error -> return + } + if (currentArticles.isEmpty()) return val ids = articleIds.toSet() - val updated = current.map { article -> + val updated = currentArticles.map { article -> if (article.id in ids) { article.copy(viewed = viewed) } else { @@ -95,7 +95,7 @@ internal class DefaultNewsRepository @Inject constructor( } } - trendingNewsStore.store(TRENDING_NEWS_KEY, updated) + trendingNewsStore.store(TRENDING_NEWS_KEY, TrendingNews.Data(updated)) } override suspend fun getCategories(): List { @@ -138,16 +138,46 @@ internal class DefaultNewsRepository @Inject constructor( } } - private suspend fun fetchAndStoreTrendingNews(limit: Int, language: String?): List { + private suspend fun fetchAndStoreTrendingNews(limit: Int, language: String?) { return withContext(dispatchers.io) { - val response = newsApi.getTrendingNews(limit = limit, language = language).getOrThrow() - val freshArticles = response.items.map { it.toDomainShortArticle() } - val currentArticles = trendingNewsStore.getSyncOrNull(TRENDING_NEWS_KEY).orEmpty() - val merged = mergeTrendingArticles(current = currentArticles, fresh = freshArticles).take(limit) - - trendingNewsStore.store(TRENDING_NEWS_KEY, merged) - - merged + val apiResponse = newsApi.getTrendingNews(limit = limit, language = language) + when (val result = apiResponse) { + is ApiResponse.Error -> { + Timber.e( + result.cause.cause, + "Trending news fetch failed cause: ${ + when (val error = result.cause) { + is ApiResponseError.HttpException -> error.code + is ApiResponseError.NetworkException -> "NetworkException" + is ApiResponseError.TimeoutException -> "TimeoutException" + is ApiResponseError.UnknownException -> "UnknownException" + } + }", + ) + trendingNewsStore.clear() + trendingNewsStore.store( + key = TRENDING_NEWS_KEY, + value = TrendingNews.Error( + NewsError.Unknown( + message = result.cause.message, + code = null, + ), + ), + ) + } + is ApiResponse.Success -> { + val freshArticles = result.data.items.map { it.toDomainShortArticle() } + val cachedArticles = trendingNewsStore.getSyncOrNull(TRENDING_NEWS_KEY) + val currentArticles = when (cachedArticles) { + is TrendingNews.Data -> cachedArticles.articles + is TrendingNews.Error -> emptyList() + null -> emptyList() + } + val merged = mergeTrendingArticles(current = currentArticles, fresh = freshArticles).take(limit) + trendingNewsStore.store(TRENDING_NEWS_KEY, TrendingNews.Data(merged)) + TrendingNews.Data(merged) + } + } } } 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/DefaultOnrampTransactionRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt index 5e515100e3..782b4b7b5b 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt @@ -1,6 +1,7 @@ package com.tangem.data.onramp import com.tangem.data.onramp.converters.TransactionConverter +import com.tangem.data.onramp.models.OnrampTerminalTransactionDTO import com.tangem.data.onramp.models.OnrampTransactionDTO import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -15,7 +16,9 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withContext +import java.util.concurrent.TimeUnit internal class DefaultOnrampTransactionRepository( private val appPreferencesStore: AppPreferencesStore, @@ -47,6 +50,7 @@ internal class DefaultOnrampTransactionRepository( override fun getAllTransactions(): Flow> { return appPreferencesStore .getObjectSet(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY) + .onStart { cleanupExpiredTerminalTransactions() } .map { transactions -> transactions.map(transactionConverter::convert) } } @@ -63,6 +67,7 @@ internal class DefaultOnrampTransactionRepository( cryptoCurrencyId: CryptoCurrency.ID, ): Flow> = appPreferencesStore .getObjectSet(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY) + .onStart { cleanupExpiredTerminalTransactions() } .map { transactions -> transactions.filter { it.userWalletId == userWalletId && it.toCurrencyId == cryptoCurrencyId.value @@ -101,4 +106,64 @@ internal class DefaultOnrampTransactionRepository( } } } + + override suspend fun isHandledTransaction(txId: String): Boolean = withContext(dispatchers.io) { + val terminated = appPreferencesStore.getObjectSetSync( + PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY, + ) + + terminated.any { it.txId == txId } + } + + override suspend fun storeHandledTransaction(txId: String) { + withContext(dispatchers.io) { + appPreferencesStore.editData { mutablePreferences -> + runCatching { + val archived = mutablePreferences.getObjectSet( + PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY, + ).orEmpty().toMutableSet() + + val record = OnrampTerminalTransactionDTO( + txId = txId, + terminatedAt = System.currentTimeMillis(), + ) + + archived.add(record) + + mutablePreferences.setObjectSet( + key = PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY, + value = archived, + ) + } + } + } + } + + private suspend fun cleanupExpiredTerminalTransactions() { + withContext(dispatchers.io) { + appPreferencesStore.editData { mutablePreferences -> + runCatching { + val archived = mutablePreferences.getObjectSet( + PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY, + )?.toMutableSet() ?: return@editData + + val oneWeekAgo = System.currentTimeMillis() - + TimeUnit.DAYS.toMillis(HANDLED_TRANSACTIONS_RETENTION_DAYS) + val cleaned = archived + .filterTo(mutableSetOf()) { it.terminatedAt > oneWeekAgo } + + if (cleaned.size != archived.size) { + mutablePreferences.setObjectSet( + key = PreferencesKeys.ONRAMP_HANDLED_TRANSACTIONS_KEY, + value = cleaned, + ) + } + } + } + } + } + + private companion object { + const val HANDLED_TRANSACTIONS_RETENTION_DAYS = 7L + } } \ No newline at end of file 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/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTerminalTransactionDTO.kt b/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTerminalTransactionDTO.kt new file mode 100644 index 0000000000..46aae3c528 --- /dev/null +++ b/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTerminalTransactionDTO.kt @@ -0,0 +1,18 @@ +package com.tangem.data.onramp.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Model for terminal onramp transaction + * + * @property txId Transaction ID + * @property terminatedAt Termination timestamp in milliseconds + */ +@JsonClass(generateAdapter = true) +data class OnrampTerminalTransactionDTO( + @Json(name = "txId") + val txId: String, + @Json(name = "terminatedAt") + val terminatedAt: Long, +) \ No newline at end of file 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 fe58a685da..31133bb96f 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 @@ -20,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 @@ -44,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 -> { @@ -89,16 +90,16 @@ internal class DefaultPromoRepository( appPreferencesStore.store(PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name), false) } - override suspend fun isMarketsStakingNotificationHideClicked(): Flow { + override fun isMarketsYieldSupplyNotificationHideClicked(): Flow { return appPreferencesStore.get( - key = PreferencesKeys.MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY, + key = PreferencesKeys.MARKETS_YIELD_SUPPLY_NOTIFICATION_HIDE_CLICKED_KEY, default = false, ) } - override suspend fun setMarketsStakingNotificationHideClicked() { + override suspend fun setMarketsYieldSupplyNotificationHideClicked() { appPreferencesStore.store( - key = PreferencesKeys.MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY, + key = PreferencesKeys.MARKETS_YIELD_SUPPLY_NOTIFICATION_HIDE_CLICKED_KEY, value = true, ) } @@ -125,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() @@ -193,8 +194,8 @@ internal class DefaultPromoRepository( const val SEPA_NAME = "sepa" const val VISA_NAME = "visa-waitlist" const val BLACK_FRIDAY_NAME = "black-friday" - const val ONE_PLUS_ONE_NAME = "one-plus-one" const val MOONPAY_NAME = "moonpay" + const val ONE_PLUS_ONE_NAME = "one-plus-one" const val STORIES_LOAD_DELAY = 1000L } } \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index dea79f4d3a..588d3b71e3 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -193,4 +193,15 @@ internal class DefaultSettingsRepository( default = false, ) } + + override suspend fun isRootDetectedWarningShown(): Boolean { + return appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.ROOT_DETECTED_WARNING_SHOWN_KEY, + default = false, + ) + } + + override suspend fun setRootDetectedWarningShown(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.ROOT_DETECTED_WARNING_SHOWN_KEY, value = value) + } } \ 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 56a6d621fb..b15bc7b0bf 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 = payInAddress, 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/build.gradle.kts b/data/visa/build.gradle.kts index 287bd65478..1470c2b5d9 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { /** Project - Data */ implementation(projects.core.datasource) implementation(projects.core.error) + implementation(projects.core.security) implementation(projects.data.common) /** Project - Domain */ 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/DefaultTangemPayEligibilityManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt new file mode 100644 index 0000000000..97a6d9e7ca --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt @@ -0,0 +1,94 @@ +package com.tangem.data.pay + +import com.tangem.common.card.FirmwareVersion +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.isLocked +import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.pay.TangemPayEligibilityManager +import com.tangem.domain.pay.repository.OnboardingRepository +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.features.hotwallet.HotWalletFeatureToggles +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import javax.inject.Inject + +internal class DefaultTangemPayEligibilityManager @Inject constructor( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val onboardingRepository: OnboardingRepository, +) : TangemPayEligibilityManager { + + private var cachedEligibleWallets: List? = null + private var eligibleWalletsDeferred: Deferred>? = null + private val loadMutex = Mutex() + + override suspend fun getEligibleWallets(): List { + cachedEligibleWallets?.let { return it } + + return loadMutex.withLock { + cachedEligibleWallets?.let { return it } + eligibleWalletsDeferred?.let { return it.await() } + + coroutineScope { + val deferred = async { + getPossibleWalletsForTangemPay() + .excludePaeraCustomers() + .also { cachedEligibleWallets = it } + } + eligibleWalletsDeferred = deferred + try { + deferred.await() + } finally { + eligibleWalletsDeferred = null + } + } + } + } + + private suspend fun getPossibleWalletsForTangemPay(): List { + if (!onboardingRepository.checkCustomerEligibility()) { + return emptyList() + } + + val wallets = if (hotWalletFeatureToggles.isHotWalletEnabled) { + userWalletsListRepository.userWallets.value + } else { + userWalletsListManager.userWalletsSync + } ?: return emptyList() + + return wallets.filter { wallet -> + wallet.isMultiCurrency && !wallet.isLocked && wallet.isCompatible() + } + } + + private fun UserWallet.isCompatible(): Boolean = when (this) { + is UserWallet.Cold -> + scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable + is UserWallet.Hot -> true + } + + private suspend fun List.excludePaeraCustomers(): List { + if (isEmpty()) return this + + return coroutineScope { + map { wallet -> + async { + val isCustomer = onboardingRepository + .checkCustomerWallet(wallet.walletId) + .getOrNull() == true + wallet to isCustomer + } + } + .awaitAll() + .mapNotNull { (wallet, isCustomer) -> + wallet.takeUnless { isCustomer } + } + } + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index bae2d97baf..ffcd79fb85 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -1,16 +1,19 @@ package com.tangem.data.pay.di import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory +import com.tangem.data.pay.DefaultTangemPayEligibilityManager import com.tangem.data.pay.repository.* import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory +import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.repository.* import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository +import com.tangem.security.DeviceSecurityInfoProvider import dagger.Binds import dagger.Module import dagger.Provides @@ -62,6 +65,10 @@ internal interface TangemPayDataModule { @Singleton fun bindTangemPayWithdrawUseCase(impl: DefaultTangemPayWithdrawUseCase): TangemPayWithdrawUseCase + @Binds + @Singleton + fun bindTangemPayEligibilityManager(impl: DefaultTangemPayEligibilityManager): TangemPayEligibilityManager + companion object { @Provides @Singleton @@ -69,11 +76,15 @@ internal interface TangemPayDataModule { repository: OnboardingRepository, customerOrderRepository: CustomerOrderRepository, tangemPayOnboardingRepository: OnboardingRepository, + eligibilityManager: TangemPayEligibilityManager, + deviceSecurity: DeviceSecurityInfoProvider, ): TangemPayMainScreenCustomerInfoUseCase { return TangemPayMainScreenCustomerInfoUseCase( repository = repository, customerOrderRepository = customerOrderRepository, tangemPayOnboardingRepository = tangemPayOnboardingRepository, + eligibilityManager = eligibilityManager, + deviceSecurity = deviceSecurity, ) } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index f3a791fb06..c3ab4f66c0 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -49,7 +49,7 @@ internal class DefaultOnboardingRepository @Inject constructor( return requestHelper.performWithStaticToken { tangemPayApi.validateDeeplink(body = DeeplinkValidityRequest(link = link)) }.map { response -> - response.result?.status == VALID_STATUS + response.result?.status.equals(VALID_STATUS, ignoreCase = true) } } @@ -84,7 +84,6 @@ internal class DefaultOnboardingRepository @Inject constructor( } override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either { - // TODO implement selector return requestHelper.performRequest(userWalletId) { authHeader -> tangemPayApi.getCustomerMe(authHeader) } .map { response -> getCustomerInfo(userWalletId = userWalletId, response = response.result) } } @@ -182,7 +181,7 @@ internal class DefaultOnboardingRepository @Inject constructor( customerWalletId = userWalletId.stringValue, ) }.map { response -> - val id = response.id + val id = response.result?.id val isPaeraCustomer = !id.isNullOrEmpty() tangemPayStorage.storeCheckCustomerWalletResult(userWalletId = userWalletId, isPaeraCustomer) isPaeraCustomer @@ -193,4 +192,19 @@ internal class DefaultOnboardingRepository @Inject constructor( error } } + + override suspend fun checkCustomerEligibility(): Boolean { + val response = requestHelper.performWithoutToken { + tangemPayApi.checkCustomerEligibility() + }.getOrNull() + return response?.result?.isTangemPayAvailable == true + } + + override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean { + return tangemPayStorage.getHideMainOnboardingBanner(userWalletId) + } + + override suspend fun setHideMainOnboardingBanner(userWalletId: UserWalletId) { + tangemPayStorage.storeHideOnboardingBanner(userWalletId, hide = true) + } } \ No newline at end of file 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 b39bff96f8..6bb55b562c 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 @@ -288,6 +288,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/repository/TangemPayRequestPerformer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt index 788aafe9d8..83e30d675e 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt @@ -89,6 +89,19 @@ internal class TangemPayRequestPerformer @Inject constructor( ) } + suspend fun performWithoutToken(requestBlock: suspend () -> ApiResponse): Either = + withContext(dispatchers.io) { + catch( + block = { + when (val apiResponse = requestBlock()) { + is ApiResponse.Error -> errorConverter.convert(apiResponse.cause).left() + is ApiResponse.Success -> apiResponse.data.right() + } + }, + catch = { errorConverter.convert(it).left() }, + ) + } + suspend fun performRequest( userWalletId: UserWalletId, requestBlock: suspend (header: String) -> ApiResponse, 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/pay/util/TangemPayErrorConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt index 8080f3b642..1b81ab80eb 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt @@ -2,7 +2,7 @@ package com.tangem.data.pay.util import com.squareup.moshi.Moshi import com.tangem.datasource.api.common.response.ApiResponseError -import com.tangem.datasource.api.pay.models.response.VisaErrorResponse +import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse import com.tangem.datasource.di.NetworkMoshi import com.tangem.domain.visa.error.VisaApiError import com.tangem.utils.converter.Converter @@ -14,7 +14,7 @@ internal class TangemPayErrorConverter @Inject constructor( @NetworkMoshi moshi: Moshi, ) : Converter { - private val visaErrorAdapter by lazy { moshi.adapter(VisaErrorResponse::class.java) } + private val tangemPayErrorAdapter by lazy { moshi.adapter(TangemPayErrorResponse::class.java) } override fun convert(value: Throwable): VisaApiError { return if (value is ApiResponseError.HttpException) { @@ -23,7 +23,7 @@ internal class TangemPayErrorConverter @Inject constructor( val errorBody = value.errorBody ?: return VisaApiError.UnknownWithoutCode return runCatching { - visaErrorAdapter.fromJson(errorBody)?.error?.code ?: value.code.numericCode + tangemPayErrorAdapter.fromJson(errorBody)?.error?.code ?: value.code.numericCode }.map { VisaApiError.fromBackendError(it) }.getOrElse { diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt deleted file mode 100644 index 919f80982d..0000000000 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayWalletsManager.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.data.pay.util - -import com.tangem.common.card.FirmwareVersion -import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.features.hotwallet.HotWalletFeatureToggles -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.first -import javax.inject.Inject - -// TODO remove after implement wallet selector in pay -class TangemPayWalletsManager @Inject constructor( - private val manager: UserWalletsListManager, - private val repository: UserWalletsListRepository, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, -) { - - @Deprecated("Don't use and put userWallet in features that need it") - suspend fun getDefaultWalletForTangemPay(): UserWallet.Cold { - val userWalletsFlow = if (useNewRepository()) repository.userWallets else manager.userWallets - val userWallets = userWalletsFlow.filter { !it.isNullOrEmpty() }.first() - return findColdWallet(userWallets) - } - - @Deprecated("Don't use and put userWallet in features that need it") - fun getDefaultWalletForTangemPayBlocking(): UserWallet.Cold { - val userWallets = if (useNewRepository()) repository.userWallets.value else manager.userWalletsSync - return findColdWallet(userWallets) - } - - private fun useNewRepository(): Boolean = hotWalletFeatureToggles.isHotWalletEnabled - - private fun findColdWallet(userWallets: List?): UserWallet.Cold { - return userWallets?.find { - it is UserWallet.Cold && it.scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable - } as? UserWallet.Cold ?: error("Cannot find cold user wallet") - } -} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultTangemPayRemoteDataSource.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultTangemPayRemoteDataSource.kt new file mode 100644 index 0000000000..c2a927964b --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultTangemPayRemoteDataSource.kt @@ -0,0 +1,87 @@ +package com.tangem.data.visa + +import arrow.core.Either +import com.squareup.moshi.Moshi +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.pay.TangemPayAuthApi +import com.tangem.datasource.api.pay.models.request.GenerateNonceByCustomerWalletRequest +import com.tangem.datasource.api.pay.models.request.GetTokenByCustomerWalletRequest +import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.domain.visa.datasource.TangemPayRemoteDataSource +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.domain.visa.model.TangemPayAuthTokens +import com.tangem.domain.visa.model.VisaAuthChallenge +import com.tangem.domain.visa.model.VisaAuthSession +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject + +internal class DefaultTangemPayRemoteDataSource @Inject constructor( + @NetworkMoshi private val moshi: Moshi, + private val tangemPayAuthApi: TangemPayAuthApi, + private val dispatchers: CoroutineDispatcherProvider, +) : TangemPayRemoteDataSource { + + private val errorAdapter by lazy { moshi.adapter(TangemPayErrorResponse::class.java) } + + override suspend fun getCustomerWalletAuthChallenge( + customerWalletAddress: String, + customerWalletId: String, + ): Either = withContext(dispatchers.io) { + request { + tangemPayAuthApi.generateNonceByCustomerWallet( + request = GenerateNonceByCustomerWalletRequest( + customerWalletAddress = customerWalletAddress, + customerWalletId = customerWalletId, + ), + ).getOrThrow() + }.map { response -> + VisaAuthChallenge.Wallet( + challenge = response.nonce, + session = VisaAuthSession(response.sessionId), + ) + } + } + + override suspend fun getTokenWithCustomerWallet( + sessionId: String, + signature: String, + nonce: String, + ): Either = withContext(dispatchers.io) { + request { + tangemPayAuthApi.getTokenByCustomerWallet( + request = GetTokenByCustomerWalletRequest( + authType = "customer_wallet", + sessionId = sessionId, + signature = signature, + messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce", + ), + ).getOrThrow() + }.map { response -> + TangemPayAuthTokens( + accessToken = response.accessToken, + expiresAt = response.expiresAt, + refreshToken = response.refreshToken, + refreshExpiresAt = response.refreshExpiresAt, + ) + } + } + + private suspend fun request(requestBlock: suspend () -> T): Either { + return runCatching { + Either.Right(requestBlock()) + }.getOrElse { responseError -> + if (responseError is ApiResponseError.HttpException && + responseError.errorBody != null + ) { + val errorCode = + errorAdapter.fromJson(responseError.errorBody)?.error?.code ?: responseError.code.numericCode + return Either.Left(VisaApiError.fromBackendError(errorCode)) + } + + return Either.Left(VisaApiError.UnknownWithoutCode) + } + } +} \ No newline at end of file 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..0b617bfe36 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 @@ -10,15 +10,16 @@ import com.tangem.datasource.api.common.config.ApiEnvironment import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.pay.TangemPayApi -import com.tangem.datasource.api.pay.models.request.* -import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter +import com.tangem.datasource.api.pay.models.request.SetPinCodeRequest +import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse +import com.tangem.datasource.api.visa.VisaApi +import com.tangem.datasource.api.visa.models.request.* import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.visa.VisaAuthTokenStorage +import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.* import com.tangem.domain.visa.repository.VisaActivationRepository -import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -29,7 +30,7 @@ import kotlinx.coroutines.withContext internal class DefaultVisaActivationRepository @AssistedInject constructor( @Assisted private val visaCardId: VisaCardId, @NetworkMoshi private val moshi: Moshi, - private val visaApi: TangemPayApi, + private val visaApi: VisaApi, private val dispatcherProvider: CoroutineDispatcherProvider, private val visaAuthTokenStorage: VisaAuthTokenStorage, private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource, @@ -37,7 +38,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( private val apiConfigsManager: ApiConfigsManager, ) : VisaActivationRepository { - private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi) + private val errorAdapter by lazy { moshi.adapter(TangemPayErrorResponse::class.java) } override suspend fun getActivationRemoteState(): Either = withContext(dispatcherProvider.io) { @@ -171,6 +172,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 @@ -186,7 +188,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( responseError.errorBody != null ) { val errorCode = - visaErrorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode + errorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode return Either.Left(VisaApiError.fromBackendError(errorCode)) } @@ -206,7 +208,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( }.getOrElse { responseError -> if (responseError is ApiResponseError.HttpException && responseError.errorBody != null) { val errorCode = - visaErrorAdapter.fromJson(responseError.errorBody!!)?.error?.code + errorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode Either.Left(VisaApiError.fromBackendError(errorCode)) } else { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRemoteDataSource.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRemoteDataSource.kt index ded2ed2d27..2f358e3a0f 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRemoteDataSource.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRemoteDataSource.kt @@ -4,33 +4,35 @@ import arrow.core.Either import com.squareup.moshi.Moshi import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.pay.TangemPayApi -import com.tangem.datasource.api.pay.TangemPayAuthApi -import com.tangem.datasource.api.pay.models.request.* -import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter +import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardIdRequest +import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse +import com.tangem.datasource.api.visa.VisaApi +import com.tangem.datasource.api.visa.models.request.* import com.tangem.datasource.di.NetworkMoshi import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.domain.visa.error.VisaApiError -import com.tangem.domain.visa.model.* +import com.tangem.domain.visa.model.VisaAuthChallenge +import com.tangem.domain.visa.model.VisaAuthSession +import com.tangem.domain.visa.model.VisaAuthSignedChallenge +import com.tangem.domain.visa.model.VisaAuthTokens import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import javax.inject.Inject internal class DefaultVisaAuthRemoteDataSource @Inject constructor( @NetworkMoshi private val moshi: Moshi, - private val visaAuthApi: TangemPayApi, - private val tangemPayAuthApi: TangemPayAuthApi, + private val visaApi: VisaApi, private val dispatchers: CoroutineDispatcherProvider, ) : VisaAuthRemoteDataSource { - private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi) + private val errorAdapter by lazy { moshi.adapter(TangemPayErrorResponse::class.java) } override suspend fun getCardAuthChallenge( cardId: String, cardPublicKey: String, ): Either = withContext(dispatchers.io) { request { - visaAuthApi.generateNonceByCardId( + visaApi.generateNonceByCardId( GenerateNoneByCardIdRequest( cardId = cardId, cardPublicKey = cardPublicKey, @@ -49,7 +51,7 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor( cardWalletAddress: String, ): Either = withContext(dispatchers.io) { request { - visaAuthApi.generateNonceByCardWallet( + visaApi.generateNonceByCardWallet( GenerateNoneByCardWalletRequest( cardWalletAddress = cardWalletAddress, cardId = cardId, @@ -63,56 +65,13 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor( } } - override suspend fun getCustomerWalletAuthChallenge( - customerWalletAddress: String, - customerWalletId: String, - ): Either = withContext(dispatchers.io) { - request { - tangemPayAuthApi.generateNonceByCustomerWallet( - request = GenerateNonceByCustomerWalletRequest( - customerWalletAddress = customerWalletAddress, - customerWalletId = customerWalletId, - ), - ).getOrThrow() - }.map { response -> - VisaAuthChallenge.Wallet( - challenge = response.nonce, - session = VisaAuthSession(response.sessionId), - ) - } - } - - override suspend fun getTokenWithCustomerWallet( - sessionId: String, - signature: String, - nonce: String, - ): Either = withContext(dispatchers.io) { - request { - tangemPayAuthApi.getTokenByCustomerWallet( - request = GetTokenByCustomerWalletRequest( - authType = "customer_wallet", - sessionId = sessionId, - signature = signature, - messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce", - ), - ).getOrThrow() - }.map { response -> - TangemPayAuthTokens( - accessToken = response.accessToken, - expiresAt = response.expiresAt, - refreshToken = response.refreshToken, - refreshExpiresAt = response.refreshExpiresAt, - ) - } - } - override suspend fun getAccessTokens( signedChallenge: VisaAuthSignedChallenge, ): Either = withContext(dispatchers.io) { request { when (signedChallenge) { is VisaAuthSignedChallenge.ByCardPublicKey -> { - visaAuthApi.getAccessTokenByCardId( + visaApi.getAccessTokenByCardId( GetAccessTokenByCardIdRequest( sessionId = signedChallenge.challenge.session.sessionId, signature = signedChallenge.signature, @@ -121,7 +80,7 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor( ).getOrThrow() } is VisaAuthSignedChallenge.ByWallet -> { - visaAuthApi.getAccessTokenByCardWallet( + visaApi.getAccessTokenByCardWallet( GetAccessTokenByCardWalletRequest( sessionId = signedChallenge.challenge.session.sessionId, signature = signedChallenge.signature, @@ -150,11 +109,11 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor( request { when (refreshToken.authType) { VisaAuthTokens.RefreshToken.Type.CardId -> - visaAuthApi.refreshCardIdAccessToken( + visaApi.refreshCardIdAccessToken( RefreshTokenByCardIdRequest(refreshToken = refreshToken.value), ) VisaAuthTokens.RefreshToken.Type.CardWallet -> - visaAuthApi.refreshCardIdAccessToken( + visaApi.refreshCardIdAccessToken( RefreshTokenByCardIdRequest(refreshToken = refreshToken.value), ) }.getOrThrow() @@ -169,7 +128,7 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor( override suspend fun exchangeAccessToken(tokens: VisaAuthTokens): Either = withContext(dispatchers.io) { request { - visaAuthApi.exchangeAccessToken( + visaApi.exchangeAccessToken( ExchangeAccessTokenRequest( accessToken = tokens.accessToken, refreshToken = tokens.refreshToken.value, @@ -194,7 +153,7 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor( responseError.errorBody != null ) { val errorCode = - visaErrorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode + errorAdapter.fromJson(responseError.errorBody!!)?.error?.code ?: responseError.code.numericCode return Either.Left(VisaApiError.fromBackendError(errorCode)) } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt index 2a81db61e6..2372e3639e 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -14,8 +14,8 @@ import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.quote.QuotesFetcher import com.tangem.data.visa.config.VisaLibLoader import com.tangem.data.visa.utils.* -import com.tangem.datasource.api.pay.TangemPayApi -import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse +import com.tangem.datasource.api.visa.VisaApi +import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet @@ -43,7 +43,7 @@ internal class DefaultVisaRepository @Inject constructor( private val userWalletsStore: UserWalletsStore, private val dispatchers: CoroutineDispatcherProvider, private val visaApiRequestMaker: VisaApiRequestMaker, - private val visaApi: TangemPayApi, + private val visaApi: VisaApi, private val visaCurrencyFactory: VisaCurrencyFactory, ) : VisaRepository { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverterWithState.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverterWithState.kt index 09128b4324..cc00430f80 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverterWithState.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverterWithState.kt @@ -1,6 +1,6 @@ package com.tangem.data.visa.converter -import com.tangem.datasource.api.pay.models.response.CardActivationRemoteStateResponse +import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse import com.tangem.domain.visa.model.VisaActivationOrderInfo import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.utils.converter.Converter diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt index a5efc3d025..0810a2640c 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt @@ -1,10 +1,12 @@ package com.tangem.data.visa.di import com.tangem.data.pay.datasource.DefaultTangemPayAuthDataSource +import com.tangem.data.visa.DefaultTangemPayRemoteDataSource import com.tangem.data.visa.DefaultVisaActivationRepository import com.tangem.data.visa.DefaultVisaAuthRemoteDataSource import com.tangem.data.visa.MockVisaRepository import com.tangem.domain.pay.datasource.TangemPayAuthDataSource +import com.tangem.domain.visa.datasource.TangemPayRemoteDataSource import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.domain.visa.repository.VisaRepository @@ -22,22 +24,16 @@ internal interface VisaDataModule { @Singleton fun bindVisaAuthRemoteDataSource(repository: DefaultVisaAuthRemoteDataSource): VisaAuthRemoteDataSource + @Binds + @Singleton + fun bindTangemPayRemoteDataSource(impl: DefaultTangemPayRemoteDataSource): TangemPayRemoteDataSource + @Binds @Singleton fun bindVisaActivationRepositoryFactory( repository: DefaultVisaActivationRepository.Factory, ): VisaActivationRepository.Factory - // Mocked - // @Binds - // @Singleton - // fun bindVisaActivationRepositoryFactory( - // repository: MockVisaActivationRepository.Factory, - // ): VisaActivationRepository.Factory - - // @Binds - // fun bindVisaRepository(repository: DefaultVisaRepository): VisaRepository - // Mocked @Binds fun bindVisaRepository(repository: MockVisaRepository): VisaRepository diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt index 04aa38e0d9..a5f2592c55 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt @@ -5,8 +5,8 @@ import com.tangem.data.visa.model.AccessCodeData import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest +import com.tangem.datasource.api.visa.VisaApi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet @@ -24,7 +24,7 @@ typealias VisaAuthorizationHeader = String internal class VisaApiRequestMaker @Inject constructor( private val userWalletsStore: UserWalletsStore, - private val visaAuthApi: TangemPayApi, + private val visaAuthApi: VisaApi, private val accessCodeDataConverter: AccessCodeDataConverter, private val dispatcherProvider: CoroutineDispatcherProvider, ) { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt index b33a0cafb9..8f9b41148f 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt @@ -2,7 +2,7 @@ package com.tangem.data.visa.utils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.externallinkprovider.TxExploreState -import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse +import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse import com.tangem.domain.visa.model.VisaTxDetails internal class VisaTxDetailsFactory { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemConverter.kt index 6311b23e70..83cc10faa3 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemConverter.kt @@ -1,6 +1,6 @@ package com.tangem.data.visa.utils -import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse +import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse import com.tangem.domain.visa.model.VisaTxHistoryItem import com.tangem.utils.converter.Converter diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt index 01950d27b7..07ebc8a190 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt @@ -3,7 +3,7 @@ package com.tangem.data.visa.utils import androidx.paging.PagingSource import androidx.paging.PagingState import com.tangem.data.common.cache.CacheRegistry -import com.tangem.datasource.api.pay.models.response.VisaTxHistoryResponse +import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.visa.model.VisaTxHistoryItem import com.tangem.utils.coroutines.CoroutineDispatcherProvider 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..d36313f946 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-13 00:34:09 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: 1271 + Modules with Issues: 69 + Average Issues per Module: 18 Progress: - Fixed: 367 out of 1802 (20%) - Remaining: 1435 + Fixed: 662 out of 1933 (34%) + Remaining: 1271 ========================================== All Modules with Issues (sorted by count) @@ -31,66 +31,73 @@ All Modules with Issues (sorted by count) Module Issues ──────────────────────────────────────────────────────────────── -features/wallet/impl 148 -features/markets/impl 148 -features/onboarding-v2/impl 130 -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 +features/markets/impl 147 +features/wallet/impl 144 +features/onboarding-v2/impl 128 +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/swap/domain 25 -data/visa 22 +features/tester/impl 28 +core/ui 26 +domain/tokens 25 +common/ui 24 features/yield-supply/impl 21 features/tangempay/details/impl 21 -data/nft 20 -features/swap/data 15 -data/wallets 14 -data/swap 13 +domain/models 21 +data/visa 21 +data/nft 19 +core/pagination 16 +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/build.gradle.kts b/domain/account/build.gradle.kts index da17a216f8..0d035a8fe5 100644 --- a/domain/account/build.gradle.kts +++ b/domain/account/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { api(projects.domain.core) api(projects.domain.models) api(projects.domain.wallets.models) + api(projects.domain.yieldSupply.models) implementation(deps.arrow.core) implementation(deps.kotlin.coroutines) 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..62c8ea8096 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, ) @@ -69,9 +70,16 @@ class ApplyAccountListSortingUseCase( return@eitherOn } - accountsCRUDRepository.saveAccounts(accountList = updatedAccountList) + applySorting(accountList = updatedAccountList) } + private suspend fun applySorting(accountList: AccountList) { + catch( + block = { accountsCRUDRepository.saveAccounts(accountList) }, + catch = { accountsCRUDRepository.saveAccountsLocally(accountList) }, + ) + } + private suspend fun Raise.getAccountList(userWalletId: UserWalletId): AccountList { return catch( block = { accountsCRUDRepository.getAccountListSync(userWalletId = userWalletId) }, 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/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt index 36b179897a..3deeee528d 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt @@ -12,7 +12,7 @@ data class TokenMarket( val isUnderMarketCapLimit: Boolean, val tokenQuotesShort: TokenQuotesShort, val tokenCharts: Charts, - val stakingRate: BigDecimal?, + val yieldRate: BigDecimal?, val updateTimestamp: Long?, private val imageHost: String, ) { diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt index 011df2c630..ffd82b8909 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt @@ -8,7 +8,7 @@ data class TokenMarketListConfig( ) { enum class Order { - ByRating, Trending, Buyers, TopGainers, TopLosers, Staking + ByRating, Trending, Buyers, TopGainers, TopLosers, Staking, YieldSupply, } enum class Interval { diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt deleted file mode 100644 index 4ccaea6680..0000000000 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetStakingNotificationMaxApyUseCase.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.domain.markets - -import android.icu.util.Calendar -import com.tangem.domain.markets.repositories.MarketsTokenRepository -import com.tangem.domain.promo.PromoRepository -import com.tangem.domain.settings.repositories.SettingsRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import java.math.BigDecimal - -class GetStakingNotificationMaxApyUseCase( - private val settingsRepository: SettingsRepository, - private val promoRepository: PromoRepository, - private val marketsTokenRepository: MarketsTokenRepository, -) { - - suspend operator fun invoke(): Flow { - val hideClickedFlow = promoRepository.isMarketsStakingNotificationHideClicked() - val walletFirstUsageDate = settingsRepository.getWalletFirstUsageDate() - val currentDate = Calendar.getInstance().timeInMillis - - return combine( - flow = hideClickedFlow, - flow2 = marketsTokenRepository.getMaxApy(), - ) { hideClicked, maxApy -> - val showStakingNotification = if (!hideClicked && walletFirstUsageDate != 0L) { - currentDate - walletFirstUsageDate > TWO_WEEKS_IN_MILLIS - } else { - false - } - - maxApy.takeIf { showStakingNotification } - } - } - - private companion object { - const val TWO_WEEKS_IN_MILLIS = 14 * 24 * 60 * 60 * 1000L - } -} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTopFiveMarketTokenUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTopFiveMarketTokenUseCase.kt new file mode 100644 index 0000000000..c81d7fcbfe --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetTopFiveMarketTokenUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.markets + +import com.tangem.domain.markets.repositories.MarketsTokenRepository + +class GetTopFiveMarketTokenUseCase( + private val marketsTokenRepository: MarketsTokenRepository, +) { + operator fun invoke( + batchingContext: TokenListBatchingContext, + order: TokenMarketListConfig.Order, + ): TokenListBatchFlow { + return marketsTokenRepository.getTokenListFlow( + batchingContext = batchingContext, + firstBatchSize = DEFAULT_BATCH_SIZE, + nextBatchSize = NEXT_BATCH_SIZE, + ) + } + + companion object { + private const val DEFAULT_BATCH_SIZE = 5 + private const val NEXT_BATCH_SIZE = 0 + } +} \ No newline at end of file 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/markets/src/main/java/com/tangem/domain/markets/ShouldShowYieldModeMarketPromoUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/ShouldShowYieldModeMarketPromoUseCase.kt new file mode 100644 index 0000000000..599e7cefc3 --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/ShouldShowYieldModeMarketPromoUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.markets + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.promo.PromoRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class ShouldShowYieldModeMarketPromoUseCase( + private val promoRepository: PromoRepository, + private val marketsTokenRepository: MarketsTokenRepository, +) { + + operator fun invoke(appCurrency: AppCurrency, interval: TokenMarketListConfig.Interval): Flow { + val hideClickedFlow = promoRepository.isMarketsYieldSupplyNotificationHideClicked() + + return hideClickedFlow.map { hideClicked -> + !hideClicked && marketsTokenRepository.showYieldModePromo( + appCurrency = appCurrency, + interval = interval, + ) + } + } +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt index d813fdcc17..00a0e86540 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -1,11 +1,10 @@ package com.tangem.domain.markets.repositories +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.* import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.coroutines.flow.Flow -import java.math.BigDecimal interface MarketsTokenRepository { @@ -56,5 +55,5 @@ interface MarketsTokenRepository { */ suspend fun getTokenExchanges(tokenId: CryptoCurrency.RawID): List - suspend fun getMaxApy(): Flow + suspend fun showYieldModePromo(appCurrency: AppCurrency, interval: TokenMarketListConfig.Interval): Boolean } \ No newline at end of file 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/news/NewsError.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/news/NewsError.kt new file mode 100644 index 0000000000..5055eacbbf --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/news/NewsError.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.models.news + +import kotlinx.serialization.Serializable + +@Serializable +sealed class NewsError { + abstract val message: String? + abstract val code: Int? + + data class ArticleNotFound( + override val message: String?, + override val code: Int?, + ) : NewsError() + + data class Unknown( + override val message: String?, + override val code: Int?, + ) : NewsError() +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/news/TrendingNews.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/news/TrendingNews.kt new file mode 100644 index 0000000000..d3b0bbdf87 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/news/TrendingNews.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.models.news + +import kotlinx.serialization.Serializable + +/** + * Represents the result of fetching news. + * Can contain either data or an error. + */ +@Serializable +sealed interface TrendingNews { + @Serializable + data class Data(val articles: List) : TrendingNews + + @Serializable + data class Error(val throwable: NewsError) : TrendingNews +} \ No newline at end of file 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..3b9e06ce82 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 @@ -1,10 +1,10 @@ package com.tangem.domain.news.repository -import com.tangem.domain.news.model.NewsListBatchFlow -import com.tangem.domain.news.model.NewsListBatchingContext import com.tangem.domain.models.news.ArticleCategory import com.tangem.domain.models.news.DetailedArticle -import com.tangem.domain.models.news.ShortArticle +import com.tangem.domain.models.news.TrendingNews +import com.tangem.domain.news.model.NewsListBatchFlow +import com.tangem.domain.news.model.NewsListBatchingContext import kotlinx.coroutines.flow.Flow /** @@ -38,22 +38,17 @@ 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 fetchTrendingNews(limit: Int, language: String?) /** * Observes trending news with runtime viewed flag support. */ - fun observeTrendingNews(): Flow> - - /** - * Refreshes trending news list and updates cache without overriding viewed status. - */ - suspend fun refreshTrendingNews(limit: Int, language: String?) + fun observeTrendingNews(): Flow /** * Updates viewed flag for provided trending articles. 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..fc69c6ba03 --- /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.fetchTrendingNews( + 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/news/src/main/java/com/tangem/domain/news/usecase/ManageTrendingNewsUseCase.kt b/domain/news/src/main/java/com/tangem/domain/news/usecase/ManageTrendingNewsUseCase.kt index 55d69e8875..6d76aaa9f6 100644 --- a/domain/news/src/main/java/com/tangem/domain/news/usecase/ManageTrendingNewsUseCase.kt +++ b/domain/news/src/main/java/com/tangem/domain/news/usecase/ManageTrendingNewsUseCase.kt @@ -1,8 +1,9 @@ package com.tangem.domain.news.usecase -import com.tangem.domain.models.news.ShortArticle +import com.tangem.domain.models.news.TrendingNews import com.tangem.domain.news.repository.NewsRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged /** * Exposes trending news as a cached flow and provides helpers to refresh or mark items as viewed. @@ -12,17 +13,12 @@ import kotlinx.coroutines.flow.Flow class ManageTrendingNewsUseCase(private val repository: NewsRepository) { /** - * Observes the current cached list of trending articles (max 10 items). + * Observes the current cached list of trending articles (max 10 items) or error. */ - operator fun invoke(): Flow> { - return repository.observeTrendingNews() - } - - /** - * Forces refresh from backend while preserving local `viewed` flags. - */ - suspend fun refresh(limit: Int, language: String?) { - repository.refreshTrendingNews(limit, language) + fun observeTrendingNews(): Flow { + return repository + .observeTrendingNews() + .distinctUntilChanged() } /** 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/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/models/src/main/kotlin/com/tangem/domain/onramp/model/error/OnrampError.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/error/OnrampError.kt index cf1f656e93..96e2f710ba 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/error/OnrampError.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/error/OnrampError.kt @@ -31,4 +31,6 @@ sealed class OnrampError { ) : OnrampError() data object PairsNotFound : OnrampError() + + data object AlreadyHandledTransaction : OnrampError() } \ No newline at end of file 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 8bc240224a..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 @@ -96,9 +96,9 @@ class GetOnrampOffersUseCase( isMoonpayPromoActive: Boolean, ): OnrampOffer? { val moonpayPromoOffers = if (isMoonpayPromoActive) { - offers.filter { - it.quote.provider.id == MOONPAY_PROMO_PROVIDER_ID && - it.quote.paymentMethod.type == PaymentMethodType.GOOGLE_PAY + offers.filter { offer -> + offer.quote.provider.id == MOONPAY_PROMO_PROVIDER_ID && + offer.quote.paymentMethod.type == PaymentMethodType.GOOGLE_PAY } } else { emptyList() diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt index 72ab25fe7c..2ac28eb227 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.onramp import arrow.core.Either +import arrow.core.left import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.onramp.repositories.OnrampErrorResolver @@ -12,6 +13,10 @@ class GetOnrampTransactionUseCase( ) { suspend operator fun invoke(txId: String): Either { + if (onrampTransactionRepository.isHandledTransaction(txId)) { + return OnrampError.AlreadyHandledTransaction.left() + } + return Either.catch { requireNotNull( onrampTransactionRepository.getTransactionById(txId), diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampRemoveTransactionUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampRemoveTransactionUseCase.kt index 3d94a2562c..2eddae74ac 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampRemoveTransactionUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampRemoveTransactionUseCase.kt @@ -11,10 +11,13 @@ class OnrampRemoveTransactionUseCase( private val errorResolver: OnrampErrorResolver, ) { - suspend operator fun invoke(txId: String?): Either { + suspend operator fun invoke(txId: String?, forceRemove: Boolean = false): Either { if (txId == null) return OnrampError.DomainError("Transaction id not provided").left() return Either.catch { + if (!forceRemove) { + onrampTransactionRepository.storeHandledTransaction(txId) + } onrampTransactionRepository.removeTransaction(txId) }.mapLeft(errorResolver::resolve) } 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/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt index c0ab3000ce..46cfdcbe6f 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt @@ -24,4 +24,8 @@ interface OnrampTransactionRepository { ) suspend fun removeTransaction(txId: String) + + suspend fun storeHandledTransaction(txId: String) + + suspend fun isHandledTransaction(txId: String): Boolean } \ No newline at end of file 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 db6aa18461..de65de82b0 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,9 @@ interface PromoRepository { suspend fun setNeverToShowTokenPromo(promoId: PromoId) - suspend fun isMarketsStakingNotificationHideClicked(): Flow + fun isMarketsYieldSupplyNotificationHideClicked(): Flow - suspend fun setMarketsStakingNotificationHideClicked() + suspend fun setMarketsYieldSupplyNotificationHideClicked() suspend fun isMoonpayPromoActive(): Boolean // endregion 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/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index 0a8f854790..68ffcaf957 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -56,4 +56,8 @@ interface SettingsRepository { suspend fun setGooglePayAvailability(value: Boolean) suspend fun isGooglePayAvailability(): Boolean + + suspend fun isRootDetectedWarningShown(): Boolean + + suspend fun setRootDetectedWarningShown(value: Boolean) } \ No newline at end of file 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/build.gradle.kts b/domain/tokens/build.gradle.kts index 68bfb40179..005eba3ef1 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(projects.domain.promo) implementation(projects.domain.networks) implementation(projects.domain.quotes) + implementation(projects.domain.yieldSupply.models) /** Project - Api */ implementation(projects.features.staking.api) 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/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index e219fbf94c..3fa0898e2b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -17,6 +17,7 @@ import com.tangem.domain.tokens.actions.OutdatedDataActionsFactory import com.tangem.domain.tokens.actions.UnreachableActionsFactory import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.yield.supply.models.YieldSupplyAvailability import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* @@ -54,7 +55,11 @@ class GetCryptoCurrencyActionsUseCase( ) @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus): Flow { + operator fun invoke( + userWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus, + yieldSupplyAvailability: YieldSupplyAvailability = YieldSupplyAvailability.Unavailable, + ): Flow { return when { cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation -> { flowOf(value = MissedDerivationsActionsFactory.create()) @@ -74,11 +79,12 @@ class GetCryptoCurrencyActionsUseCase( userWalletId = userWallet.walletId, currency = cryptoCurrencyStatus.currency, ) - .mapLatest { + .mapLatest { stakingAvailability -> outdatedDataActionsFactory.create( userWallet = userWallet, cryptoCurrencyStatus = cryptoCurrencyStatus, - stakingAvailability = it, + stakingAvailability = stakingAvailability, + yieldSupplyAvailability = yieldSupplyAvailability, ) } } @@ -94,6 +100,7 @@ class GetCryptoCurrencyActionsUseCase( userWallet = userWallet, cryptoCurrencyStatus = cryptoCurrencyStatus, stakingAvailability = stakingAvailability, + yieldSupplyAvailability = yieldSupplyAvailability, shouldShowSwapStories = swapStoryContent != null, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt index d18e2e36f4..c3850162db 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt @@ -11,6 +11,7 @@ import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState.ActionState import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.yield.supply.models.YieldSupplyAvailability import kotlinx.coroutines.Deferred import kotlinx.coroutines.withTimeoutOrNull @@ -193,4 +194,13 @@ internal open class BaseActionsFactory( is AssetRequirementsCondition.RequiredTrustline -> ScenarioUnavailabilityReason.TrustlineRequired } } + + protected fun ActionAvailabilityBuilder.addYieldSupplyAction(yieldSupplyAvailability: YieldSupplyAvailability) { + if (yieldSupplyAvailability is YieldSupplyAvailability.Available) { + ActionState.YieldMode( + unavailabilityReason = ScenarioUnavailabilityReason.None, + apy = yieldSupplyAvailability.apy, + ).addByReason() + } + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt index bef54bd155..3fb574d9e2 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt @@ -9,6 +9,7 @@ import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState.ActionState import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.yield.supply.models.YieldSupplyAvailability import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -32,12 +33,14 @@ internal class CommonActionsFactory( * @param userWallet the user's cold wallet * @param cryptoCurrencyStatus the status of the cryptocurrency * @param stakingAvailability the staking availability for the cryptocurrency + * @param yieldSupplyAvailability the yield supply availability for the cryptocurrency * @param shouldShowSwapStories a flag indicating whether to show swap stories */ suspend fun create( userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus, stakingAvailability: StakingAvailability, + yieldSupplyAvailability: YieldSupplyAvailability, shouldShowSwapStories: Boolean, ): Set = coroutineScope { val isAddressAvailable = isAddressAvailable(cryptoCurrencyStatus.value.networkAddress) @@ -128,6 +131,10 @@ internal class CommonActionsFactory( // region HideToken addHideTokenAction() // endregion + + // region YieldMode + addYieldSupplyAction(yieldSupplyAvailability) + // endregion } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt index f6ff91d2fd..308a2812bd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt @@ -8,6 +8,7 @@ import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState.ActionState import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.yield.supply.models.YieldSupplyAvailability import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -31,11 +32,13 @@ internal class OutdatedDataActionsFactory( * @param userWallet the user's cold wallet * @param cryptoCurrencyStatus the status of the cryptocurrency * @param stakingAvailability the staking availability for the cryptocurrency + * @param yieldSupplyAvailability the yield supply availability for the cryptocurrency */ suspend fun create( userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus, stakingAvailability: StakingAvailability, + yieldSupplyAvailability: YieldSupplyAvailability, ): Set = coroutineScope { val sources = cryptoCurrencyStatus.value.sources @@ -129,6 +132,10 @@ internal class OutdatedDataActionsFactory( // region HideToken addHideTokenAction() // endregion + + // region Yield Mode + addYieldSupplyAction(yieldSupplyAvailability) + // endregion } } 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/model/TokenActionsState.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt index 1260157432..1cea4893f6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt @@ -34,6 +34,11 @@ data class TokenActionsState( data class Send(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() + data class YieldMode( + override val unavailabilityReason: ScenarioUnavailabilityReason, + val apy: String, + ) : ActionState() + data class Analytics(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() data class HideToken(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() 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/build.gradle.kts b/domain/visa/build.gradle.kts index 1f83cc44b1..3c13ebe644 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -12,9 +12,10 @@ android { dependencies { /** Project - Core */ api(projects.core.pagination) - implementation(projects.core.utils) - implementation(projects.core.error) implementation(projects.core.analytics.models) + implementation(projects.core.error) + implementation(projects.core.security) + implementation(projects.core.utils) /** Project - Domain */ api(projects.domain.models) 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/pay/TangemPayEligibilityManager.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt new file mode 100644 index 0000000000..b68cd25513 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.pay + +import com.tangem.domain.models.wallet.UserWallet + +interface TangemPayEligibilityManager { + + suspend fun getEligibleWallets(): List +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index ad9583243f..d747e41066 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -5,6 +5,7 @@ import java.math.BigDecimal sealed class MainCustomerInfoContentState { object Loading : MainCustomerInfoContentState() + object OnboardingBanner : MainCustomerInfoContentState() data class Content(val info: MainScreenCustomerInfo) : MainCustomerInfoContentState() } diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCustomerInfoError.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCustomerInfoError.kt index 7fda6cfe51..e2086cfed3 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCustomerInfoError.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCustomerInfoError.kt @@ -5,4 +5,5 @@ sealed interface TangemPayCustomerInfoError { data object UnavailableError : TangemPayCustomerInfoError data object RefreshNeededError : TangemPayCustomerInfoError data object UnknownError : TangemPayCustomerInfoError + data object ExposedDeviceError : TangemPayCustomerInfoError } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt index 58046210b6..e3fc78e653 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt @@ -25,5 +25,11 @@ interface OnboardingRepository { suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either + suspend fun checkCustomerEligibility(): Boolean + fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? + + suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean + + suspend fun setHideMainOnboardingBanner(userWalletId: UserWalletId) } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index 07950a5cec..10a21fbb78 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -4,10 +4,13 @@ import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.model.* import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.visa.error.VisaApiError +import com.tangem.security.DeviceSecurityInfoProvider +import com.tangem.security.isSecurityExposed import kotlinx.coroutines.flow.* import timber.log.Timber @@ -21,6 +24,8 @@ class TangemPayMainScreenCustomerInfoUseCase( private val repository: OnboardingRepository, private val customerOrderRepository: CustomerOrderRepository, private val tangemPayOnboardingRepository: OnboardingRepository, + private val eligibilityManager: TangemPayEligibilityManager, + private val deviceSecurity: DeviceSecurityInfoProvider, ) { val state: StateFlow>> @@ -28,6 +33,16 @@ class TangemPayMainScreenCustomerInfoUseCase( suspend fun fetch(userWalletId: UserWalletId) { Timber.tag(TAG).i("fetch: $userWalletId") + + if (deviceSecurity.isSecurityExposed()) { + Timber.tag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") + Timber.tag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}") + Timber.tag(TAG).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") + + updateState(userWalletId = userWalletId, either = TangemPayCustomerInfoError.ExposedDeviceError.left()) + return // fast exit + } + repository.checkCustomerWallet(userWalletId) .fold( ifLeft = { error -> @@ -46,8 +61,17 @@ class TangemPayMainScreenCustomerInfoUseCase( .map(MainCustomerInfoContentState::Content) updateState(userWalletId, result) } else { - // ignore if there's no TangemPay - updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left()) + // if there's no tangem pay, check eligibility and show onboarding banner + val isEligible = eligibilityManager.getEligibleWallets().any { it.walletId == userWalletId } + if (isEligible) { + if (tangemPayOnboardingRepository.getHideMainOnboardingBanner(userWalletId)) { + updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left()) + } else { + updateState(userWalletId, MainCustomerInfoContentState.OnboardingBanner.right()) + } + } else { + updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left()) + } } }, ) 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/visa/src/main/kotlin/com/tangem/domain/visa/datasource/TangemPayRemoteDataSource.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/TangemPayRemoteDataSource.kt new file mode 100644 index 0000000000..ac5eeedaa1 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/TangemPayRemoteDataSource.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.visa.datasource + +import arrow.core.Either +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.domain.visa.model.TangemPayAuthTokens +import com.tangem.domain.visa.model.VisaAuthChallenge + +interface TangemPayRemoteDataSource { + + suspend fun getCustomerWalletAuthChallenge( + customerWalletAddress: String, + customerWalletId: String, + ): Either + + suspend fun getTokenWithCustomerWallet( + sessionId: String, + signature: String, + nonce: String, + ): Either +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/VisaAuthRemoteDataSource.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/VisaAuthRemoteDataSource.kt index c5cbbee4b9..e559eb80aa 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/VisaAuthRemoteDataSource.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/datasource/VisaAuthRemoteDataSource.kt @@ -2,7 +2,6 @@ package com.tangem.domain.visa.datasource import arrow.core.Either import com.tangem.domain.visa.error.VisaApiError -import com.tangem.domain.visa.model.TangemPayAuthTokens import com.tangem.domain.visa.model.VisaAuthChallenge import com.tangem.domain.visa.model.VisaAuthSignedChallenge import com.tangem.domain.visa.model.VisaAuthTokens @@ -19,17 +18,6 @@ interface VisaAuthRemoteDataSource { cardWalletAddress: String, ): Either - suspend fun getCustomerWalletAuthChallenge( - customerWalletAddress: String, - customerWalletId: String, - ): Either - - suspend fun getTokenWithCustomerWallet( - sessionId: String, - signature: String, - nonce: String, - ): Either - suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): Either suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): Either 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/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyAvailability.kt b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyAvailability.kt new file mode 100644 index 0000000000..aa74eb5448 --- /dev/null +++ b/domain/yield-supply/models/src/main/java/com/tangem/domain/yield/supply/models/YieldSupplyAvailability.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.yield.supply.models + +/** + * Represents the availability of yield supply for a given asset. + */ +sealed class YieldSupplyAvailability { + data class Available(val apy: String) : YieldSupplyAvailability() + + data object Unavailable : YieldSupplyAvailability() +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetAvailabilityUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetAvailabilityUseCase.kt new file mode 100644 index 0000000000..db9db0ea7e --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetAvailabilityUseCase.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.yield.supply.usecase + +import arrow.core.Either +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.yieldSupplyKey +import com.tangem.domain.yield.supply.YieldSupplyRepository +import com.tangem.domain.yield.supply.models.YieldSupplyAvailability + +class YieldSupplyGetAvailabilityUseCase( + private val yieldSupplyRepository: YieldSupplyRepository, +) { + + suspend operator fun invoke(currency: CryptoCurrency): Either = Either.catch { + val tokenCurrency = currency as? CryptoCurrency.Token ?: return@catch YieldSupplyAvailability.Unavailable + + val tokens = yieldSupplyRepository.getCachedMarkets().orEmpty() + val cachedStatus = tokens.firstOrNull { it.yieldSupplyKey == tokenCurrency.yieldSupplyKey() } + val yieldSupplyToken = cachedStatus ?: yieldSupplyRepository.getTokenStatus(tokenCurrency) + + if (yieldSupplyToken.isActive) { + YieldSupplyAvailability.Available(yieldSupplyToken.apy.toString()) + } else { + YieldSupplyAvailability.Unavailable + } + } +} \ No newline at end of file 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/YieldSupplyGetCurrentFeeUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt index ee27141e2d..54977eb022 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt @@ -367,7 +367,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest { fiatAmount = BigDecimal.ZERO, fiatRate = fiatRate, 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 d8b29bfd94..878b0ca49e 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 @@ -1,10 +1,10 @@ package com.tangem.domain.yield.supply.usecase import com.google.common.truth.Truth.assertThat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.anyDecimals import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -18,8 +18,8 @@ import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.async -import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy @@ -60,7 +60,7 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { fiatAmount = null, fiatRate = BigDecimal.ONE, priceChange = null, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -92,7 +92,7 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { fiatAmount = null, fiatRate = BigDecimal.ONE, priceChange = null, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -123,7 +123,7 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { fiatAmount = null, fiatRate = BigDecimal.ONE, priceChange = null, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -168,7 +168,7 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { fiatAmount = null, fiatRate = BigDecimal.ONE, priceChange = null, - yieldBalance = null, + stakingBalance = null, yieldSupplyStatus = null, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), @@ -275,7 +275,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..cddcad417a 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,9 @@ 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.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -22,6 +25,7 @@ import com.tangem.core.ui.components.label.entity.LabelStyle import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -71,6 +75,7 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi Column( modifier = Modifier .weight(1f) + .verticalScroll(rememberScrollState()) .padding( start = 16.dp, top = 24.dp, @@ -113,6 +118,7 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi } } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun WalletBlock( title: String, @@ -137,11 +143,11 @@ private fun WalletBlock( vertical = 12.dp, ), ) { - Row { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { Text( - modifier = Modifier - .weight(1f, fill = false) - .padding(end = 8.dp), text = title, style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, @@ -176,6 +182,7 @@ private fun WalletBlock( private fun Feature(feature: CreateWalletSelectionUM.Feature, modifier: Modifier = Modifier) { Row( modifier = modifier, + verticalAlignment = Alignment.CenterVertically, ) { Icon( modifier = Modifier.size(TangemTheme.dimens.size16), @@ -221,6 +228,8 @@ private fun AlreadyHaveTangemWalletBlock( text = stringResourceSafe(R.string.wallet_add_hardware_purchase), style = TangemTheme.typography.button, color = TangemTheme.colors.text.primary1, + maxLines = 3, + overflow = TextOverflow.Ellipsis, ) SecondaryButton( @@ -242,7 +251,7 @@ private fun PreviewCreateWalletContent() { onBackClick = { }, blocks = persistentListOf( CreateWalletSelectionUM.Block( - title = resourceReference(R.string.wallet_create_hardware_title), + title = stringReference("Hardware wallet very long title"), titleLabel = LabelUM( text = resourceReference(R.string.common_recommended), style = LabelStyle.ACCENT, @@ -277,6 +286,7 @@ private fun PreviewCreateWalletContent() { onClick = { }, ), ), + shouldShowAlreadyHaveWallet = true, onBuyClick = { }, ), ) 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/build.gradle.kts b/features/details/impl/build.gradle.kts index 8f41e0f6c1..8da459c047 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(projects.features.tester.api) implementation(projects.features.createWalletSelection.api) implementation(projects.features.hotWallet.api) + implementation(projects.features.tangempay.details.api) /* Project - Core */ implementation(projects.core.decompose) @@ -46,6 +47,7 @@ dependencies { implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) implementation(projects.domain.legacy) + implementation(projects.domain.visa) /* SDK */ // TODO: For TangemError model, should be removed after card domain scanning refactoring 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..69fe323b69 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,15 @@ 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.pay.TangemPayEligibilityManager 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 +34,8 @@ 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.features.tangempay.TangemPayFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.version.AppVersionProvider import kotlinx.collections.immutable.ImmutableList @@ -46,7 +53,7 @@ import javax.inject.Inject @Suppress("LongParameterList") internal class DetailsModel @Inject constructor( socialsBuilder: SocialsBuilder, - itemsBuilder: ItemsBuilder, + private val itemsBuilder: ItemsBuilder, private val appVersionProvider: AppVersionProvider, private val checkIsWalletConnectAvailableUseCase: CheckIsWalletConnectAvailableUseCase, private val router: Router, @@ -60,6 +67,11 @@ 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, + private val tangemPayEligibilityManager: TangemPayEligibilityManager, + private val tangemPayFeatureToggles: TangemPayFeatureToggles, ) : Model() { private val params: DetailsComponent.Params = paramsContainer.require() @@ -92,6 +104,8 @@ internal class DetailsModel @Inject constructor( ), ) + addTangemPayItemIfEligible() + state = MutableStateFlow( value = DetailsUM( items = items.value, @@ -216,7 +230,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()) + } } } @@ -226,6 +245,16 @@ internal class DetailsModel @Inject constructor( } } + private fun addTangemPayItemIfEligible() { + if (!tangemPayFeatureToggles.isTangemPayEnabled) return + modelScope.launch { + val isEligible = tangemPayEligibilityManager.getEligibleWallets().isNotEmpty() + if (isEligible) { + items.update { itemsBuilder.addVisaItem(it) } + } + } + } + private fun getAppVersion(): String = "${appVersionProvider.versionName} (${appVersionProvider.versionCode})" private suspend fun buildBuyLink(): String { 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..badebd605e 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.Settings)) router.push(AppRoute.CreateWalletSelection) } else { withProgress(isWalletSavingInProgress) { @@ -104,7 +109,9 @@ internal class UserWalletListModel @Inject constructor( Timber.e("Failed to unlock wallet $userWalletId: $error") error.handle( onUserCancelled = {}, + isFromUnlockAll = false, 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/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index 617de52a47..0cdd6b6a10 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -37,6 +37,17 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) { ).let(::add) }.toImmutableList() + fun addVisaItem(items: ImmutableList): ImmutableList { + return items.toMutableList().map { block -> + if (block.id == "shop" && block is DetailsItemUM.Basic) { + val newItems = block.items.toMutableList().apply { add(getVisaItem()) } + block.copy(items = newItems.toImmutableList()) + } else { + block + } + }.toImmutableList() + } + private fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean, userWalletId: UserWalletId): DetailsItemUM? { return if (isWalletConnectAvailable) { DetailsItemUM.WalletConnect( @@ -114,4 +125,15 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) { ).let(::add) }.toPersistentList(), ) + + private fun getVisaItem(): DetailsItemUM.Basic.Item = DetailsItemUM.Basic.Item( + id = "get_tangem_visa", + block = BlockUM( + text = resourceReference(R.string.details_get_visa), + iconRes = R.drawable.ic_tangem_pay_24, + onClick = { + router.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings)) + }, + ), + ) } \ No newline at end of file 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..9d6659788e 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,11 +87,13 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.navigation) + implementation(projects.core.utils) /* Common */ implementation(projects.common.ui) implementation(projects.common.uiCharts) implementation(projects.common.routing) + implementation(projects.common.uiMarkets) /* Libs */ implementation(projects.libs.crypto) 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..dbcfde3de2 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,15 @@ 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.model.feed.FeedModelClickIntents +import com.tangem.features.feed.ui.EntryBottomSheetContent +import com.tangem.features.feed.ui.market.state.SortByTypeUM import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -35,7 +42,37 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( popCallback = { onChildBack() }, ) - private val stack: Value> = childStack( + private val clickIntents = object : FeedEntryClickIntents { + override fun onMarketItemClick(token: TokenMarketParams, appCurrency: AppCurrency) { + innerRouter.push( + route = FeedEntryChildFactory.Child.TokenDetails( + params = DefaultMarketsTokenDetailsComponent.Params( + token = token, + appCurrency = appCurrency, + shouldShowPortfolio = true, + analyticsParams = DefaultMarketsTokenDetailsComponent.AnalyticsParams( + blockchain = null, + source = "Market", + ), + ), + ), + ) + } + + override fun onMarketOpenClick(sortBy: SortByTypeUM) { + innerRouter.push(FeedEntryChildFactory.Child.TokenList) + } + + override fun onArticleClick(articleId: Int) { + innerRouter.push(FeedEntryChildFactory.Child.NewsDetails) + } + + override fun onOpenAllNews() { + innerRouter.push(FeedEntryChildFactory.Child.NewsList) + } + } + + private val stack: Value> = childStack( key = "main", source = stackNavigation, serializer = FeedEntryChildFactory.Child.serializer(), @@ -48,7 +85,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( componentContext = factoryContext, router = innerRouter, ), - onTokenClick = ::marketsListTokenSelected, + feedEntryClickIntents = clickIntents, ) }, ) @@ -59,22 +96,15 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier, ) { - bottomSheetState // TODO will be continued in next tasks. - } + val stackState by stack.subscribeAsState() - private fun marketsListTokenSelected(token: TokenMarketParams, appCurrency: AppCurrency) { - innerRouter.push( - route = FeedEntryChildFactory.Child.TokenDetails( - params = DefaultMarketsTokenDetailsComponent.Params( - token = token, - appCurrency = appCurrency, - shouldShowPortfolio = true, - analyticsParams = DefaultMarketsTokenDetailsComponent.AnalyticsParams( - blockchain = null, - source = "Market", - ), - ), - ), + BackHandler(enabled = bottomSheetState.value == BottomSheetState.EXPANDED) { + onChildBack() + } + + EntryBottomSheetContent( + stackState = stackState, + onHeaderSizeChange = onHeaderSizeChange, ) } @@ -88,4 +118,6 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( interface Factory : FeedEntryComponent.Factory { override fun create(context: AppComponentContext): DefaultFeedEntryComponent } -} \ No newline at end of file +} + +internal interface FeedEntryClickIntents : FeedModelClickIntents \ No newline at end of file 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..081ec2ab4d 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,16 +3,16 @@ 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.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.TokenMarketParams +import com.tangem.core.ui.decompose.ComposableModularContentComponent import com.tangem.features.feed.components.feed.DefaultFeedComponent import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent 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 @@ -42,8 +42,8 @@ internal class FeedEntryChildFactory { fun createChild( child: Child, appComponentContext: AppComponentContext, - onTokenClick: (TokenMarketParams, AppCurrency) -> Unit, - ): Any { + feedEntryClickIntents: FeedEntryClickIntents, + ): ComposableModularContentComponent { return when (child) { is Child.TokenDetails -> { DefaultMarketsTokenDetailsComponent( @@ -54,7 +54,12 @@ internal class FeedEntryChildFactory { is Child.TokenList -> { DefaultMarketsTokenListComponent( appComponentContext = appComponentContext, - onTokenClick = onTokenClick, + onTokenClick = { token, appCurrency -> + feedEntryClickIntents.onMarketItemClick( + token, + appCurrency, + ) + }, ) } Child.NewsDetails -> { @@ -70,6 +75,7 @@ internal class FeedEntryChildFactory { Child.Feed -> { DefaultFeedComponent( appComponentContext = appComponentContext, + params = DefaultFeedComponent.FeedParams(feedClickIntents = feedEntryClickIntents), ) } } 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..0b6ca24074 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,49 @@ 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.LifecycleStartEffect +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.model.feed.FeedModelClickIntents +import com.tangem.features.feed.ui.feed.FeedList +import com.tangem.features.feed.ui.feed.FeedListHeader internal class DefaultFeedComponent( appComponentContext: AppComponentContext, + private val params: FeedParams, ) : ComposableModularContentComponent, AppComponentContext by appComponentContext { + private val feedComponentModel = getOrCreateModel(params = params) + @Composable override fun Title() { + val state by feedComponentModel.state.collectAsStateWithLifecycle() + FeedListHeader(state.searchBar) } @Composable override fun Content(modifier: Modifier) { + LifecycleStartEffect(Unit) { + feedComponentModel.isVisibleOnScreen.value = true + onStopOrDispose { + feedComponentModel.isVisibleOnScreen.value = false + } + } + + val state by feedComponentModel.state.collectAsStateWithLifecycle() + FeedList( + modifier = modifier, + state = state, + ) } @Composable - override fun Footer() { - } + override fun Footer() = Unit + + data class FeedParams(val feedClickIntents: FeedModelClickIntents) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/FeedComponentModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/FeedComponentModule.kt new file mode 100644 index 0000000000..7d5c10f82d --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/FeedComponentModule.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 FeedComponentModule { + + @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/converter/MarketsTokenItemConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt new file mode 100644 index 0000000000..6bc58aab6e --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt @@ -0,0 +1,159 @@ +package com.tangem.features.feed.model.converter + +import com.tangem.common.ui.charts.state.MarketChartData +import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter +import com.tangem.common.ui.charts.state.sorted +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.* +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarket +import com.tangem.features.feed.impl.R +import com.tangem.features.feed.ui.market.state.MarketsListUM +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal +import java.math.RoundingMode + +internal class MarketsTokenItemConverter( + private val currentTrendInterval: MarketsListUM.TrendInterval, + private val appCurrency: AppCurrency, +) : Converter { + + private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(shouldFormatAxis = false) + + override fun convert(value: TokenMarket): MarketsListItemUM { + return MarketsListItemUM( + id = value.id, + name = value.name, + currencySymbol = value.symbol, + ratingPosition = value.marketRating?.toString(), + marketCap = value.getMarketCap(), + iconUrl = value.imageUrlLarge, + price = value.getCurrentPrice(), + trendPercentText = value.getTrendPercent(), + trendType = value.getTrendType(), + chartData = value.getChartData(), + isUnder100kMarketCap = value.isUnderMarketCapLimit, + stakingRate = value.yieldRate?.format { percent() }?.let { + resourceReference(R.string.markets_apy_placeholder, wrappedList(it)) + }, + updateTimestamp = value.updateTimestamp, + ) + } + + fun update(prev: TokenMarket, prevUI: MarketsListItemUM, new: TokenMarket): MarketsListItemUM { + require(prev.id == new.id) { + "Ids is not the same during update TokenMarket item: previousItem[${prev.id}] != newItem[${new.id}]" + } + + return prevUI.copy( + name = new.name, + currencySymbol = new.symbol, + ratingPosition = new.marketRating?.toString(), + marketCap = ifChanged(prev.marketCap, new.marketCap, prevUI.marketCap) { new.getMarketCap() }, + iconUrl = new.imageUrlLarge, + price = ifChanged(prev = prev.tokenQuotesShort, new = new.tokenQuotesShort, prevR = prevUI.price) { + new.getCurrentPrice( + prev = prev, + ) + }, + trendPercentText = ifChanged( + prev.tokenQuotesShort, + new.tokenQuotesShort, + prevUI.trendPercentText, + ) { new.getTrendPercent() }, + trendType = ifChanged(prev.tokenQuotesShort, new.tokenQuotesShort, prevUI.trendType) { new.getTrendType() }, + chartData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chartData) { new.getChartData() }, + ) + } + + private inline fun ifChanged(prev: T, new: T, prevR: R, force: Boolean = false, change: (T) -> R): R { + return if (force || prev != new) change(new) else prevR + } + + private fun TokenMarket.getMarketCap(): String? { + val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null + + return value.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).compact( + threeDigitsMethod = true, + ) + } + } + + private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price { + val prevPrice = prev?.tokenQuotesShort?.currentPrice + + val priceText = tokenQuotesShort.currentPrice.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).price() + } + + val changeType = if (prevPrice != null) { + if (tokenQuotesShort.currentPrice > prevPrice) { + PriceChangeType.UP + } else { + PriceChangeType.DOWN + } + } else { + null + } + + return MarketsListItemUM.Price( + text = priceText, + changeType = changeType, + ) + } + + private fun TokenMarket.getChartData(): MarketChartRawData? { + val chart = when (currentTrendInterval) { + MarketsListUM.TrendInterval.H24 -> tokenCharts.h24 + MarketsListUM.TrendInterval.D7 -> tokenCharts.week + MarketsListUM.TrendInterval.M1 -> tokenCharts.month + } + + return chart?.let { ct -> + priceAndTimePointValuesConverter.convert( + MarketChartData.Data( + y = ct.priceY.toImmutableList(), + x = ct.timeStamps.map { it.toBigDecimal() }.toImmutableList(), + ).sorted(), + ) + } + } + + @Suppress("MagicNumber") + private fun TokenMarket.getTrendType(): PriceChangeType { + val percent = when (currentTrendInterval) { + MarketsListUM.TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent + MarketsListUM.TrendInterval.D7 -> tokenQuotesShort.weekChangePercent + MarketsListUM.TrendInterval.M1 -> tokenQuotesShort.monthChangePercent + } + val scaled = percent?.setScale(4, RoundingMode.HALF_UP) + return when { + scaled == null -> PriceChangeType.NEUTRAL + scaled > BigDecimal.ZERO -> PriceChangeType.UP + scaled < BigDecimal.ZERO -> PriceChangeType.DOWN + else -> PriceChangeType.NEUTRAL + } + } + + private fun TokenMarket.getTrendPercent(): String { + val percent = when (currentTrendInterval) { + MarketsListUM.TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent + MarketsListUM.TrendInterval.D7 -> tokenQuotesShort.weekChangePercent + MarketsListUM.TrendInterval.M1 -> tokenQuotesShort.monthChangePercent + } + + return percent.format { percent() } + } +} \ 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..6b7fb68da4 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt @@ -0,0 +1,348 @@ +package com.tangem.features.feed.model.feed + +import androidx.compose.runtime.Stable +import arrow.core.getOrElse +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.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.GetTopFiveMarketTokenUseCase +import com.tangem.domain.markets.TokenMarketListConfig +import com.tangem.domain.markets.toSerializableParam +import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase +import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase +import com.tangem.features.feed.components.feed.DefaultFeedComponent +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.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentHashMap +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* +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, + getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase, + getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + paramsContainer: ParamsContainer, +) : Model() { + + private val params = paramsContainer.require() + + private var quotesUpdateJob: Job? = null + + private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + }.stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + + private val marketsBatchFlowManager = FeedMarketsBatchFlowManager( + getTopFiveMarketTokenUseCase = getTopFiveMarketTokenUseCase, + currentAppCurrency = Provider { currentAppCurrency.value }, + modelScope = modelScope, + dispatchers = dispatchers, + ) + + internal val state: StateFlow + field = MutableStateFlow(initialState()) + + 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 } }, + ) + } + + val isVisibleOnScreen = MutableStateFlow(false) + + init { + updateCallbacks() + + modelScope.launch(dispatchers.default) { + fetchTrendingNewsUseCase() + } + + modelScope.launch(dispatchers.default) { + combine( + flow = marketsBatchFlowManager.itemsByOrder, + flow2 = marketsBatchFlowManager.loadingStatesByOrder, + flow3 = marketsBatchFlowManager.errorStatesByOrder, + flow4 = manageTrendingNewsUseCase.observeTrendingNews(), + ) { itemsByOrder, loadingStatesByOrder, errorStatesByOrder, trendingNewsResult -> + updateMarketCharts(itemsByOrder, loadingStatesByOrder, errorStatesByOrder) + trendingNewsStateFactory.updateTrendingNewsState( + result = trendingNewsResult, + onRetryClicked = { + modelScope.launch(dispatchers.default) { + fetchTrendingNewsUseCase.invoke() + } + }, + ) + updateGlobalState() + val currentSortType = state.value.marketChartConfig.currentSortByType + val items = itemsByOrder[currentSortType] + val isLoading = loadingStatesByOrder[currentSortType] == true + if (items != null && items.isNotEmpty() && !isLoading) { + val order = currentSortType.toOrder() + marketsBatchFlowManager.loadCharts(order) + } + }.collect() + } + + modelScope.launch(dispatchers.default) { + currentAppCurrency.drop(1).collect { + marketsBatchFlowManager.reloadAll() + } + } + + modelScope.launch(dispatchers.default) { + TokenMarketListConfig.Order.entries.forEach { order -> + marketsBatchFlowManager.getOnLastBatchLoadedSuccessFlow(order)?.collect { batchKey -> + marketsBatchFlowManager.loadCharts(order) + if (batchKey == 0) { + startQuotesUpdateTimer() + } + } + } + } + } + + 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) + } + }.toPersistentHashMap(), + currentSortByType = SortByTypeUM.Trending, + ), + globalState = GlobalFeedState.Loading, + ) + } + + private fun getCurrentDate(): String { + val localDate = DateTime(DateTime.now(), DateTimeZone.getDefault()) + return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate) + } + + private fun updateMarketCharts( + itemsByOrder: Map>, + loadingStatesByOrder: Map, + errorStatesByOrder: Map, + ) { + state.update { currentState -> + val newMarketCharts = buildMap { + SortByTypeUM.entries.forEach { sortByType -> + val items = itemsByOrder[sortByType] ?: persistentListOf() + val isLoading = loadingStatesByOrder[sortByType] == true + val hasError = errorStatesByOrder[sortByType] == true + + when { + hasError -> { + put( + sortByType, + MarketChartUM.LoadingError( + onRetryClicked = { + marketsBatchFlowManager.reloadAll() + }, + ), + ) + } + isLoading -> { + put(sortByType, MarketChartUM.Loading) + } + items.isEmpty() -> { + put( + sortByType, + MarketChartUM.LoadingError( + onRetryClicked = { + marketsBatchFlowManager.reloadAll() + }, + ), + ) + } + else -> { + put( + sortByType, + MarketChartUM.Content( + items = items, + sortChartConfig = SortChartConfigUM( + sortByType = sortByType, + isSelected = sortByType == currentState.marketChartConfig.currentSortByType, + ), + ), + ) + } + } + } + }.toPersistentHashMap() + + currentState.copy( + marketChartConfig = currentState.marketChartConfig.copy( + marketCharts = newMarketCharts, + ), + ) + } + } + + private fun updateGlobalState() { + state.update { currentState -> + val newsState = currentState.news + val marketCharts = currentState.marketChartConfig.marketCharts + + val isNewsLoading = newsState is NewsUM.Loading + val areAllChartsLoading = marketCharts.values.all { it is MarketChartUM.Loading } + + val isNewsError = newsState is NewsUM.Error + val areAllChartsError = marketCharts.values.all { it is MarketChartUM.LoadingError } + + val newGlobalState = when { + isNewsLoading && areAllChartsLoading -> GlobalFeedState.Loading + isNewsError && areAllChartsError -> GlobalFeedState.Error( + onRetryClicked = { + modelScope.launch(dispatchers.default) { + fetchTrendingNewsUseCase.invoke() + marketsBatchFlowManager.reloadAll() + } + }, + ) + else -> GlobalFeedState.Content + } + + val currentGlobalState = currentState.globalState + if (currentGlobalState::class != newGlobalState::class) { + currentState.copy(globalState = newGlobalState) + } else { + currentState + } + } + } + + private fun onSortTypeClick(sortByType: SortByTypeUM) { + state.update { currentState -> + val updatedCharts = currentState.marketChartConfig.marketCharts.mapValues { (chartSortType, chart) -> + when (chart) { + is MarketChartUM.Content -> { + chart.copy( + sortChartConfig = chart.sortChartConfig.copy( + isSelected = chartSortType == sortByType, + ), + ) + } + else -> chart + } + } + + currentState.copy( + marketChartConfig = currentState.marketChartConfig.copy( + currentSortByType = sortByType, + marketCharts = updatedCharts.toPersistentHashMap(), + ), + ) + } + modelScope.launch(dispatchers.default) { + marketsBatchFlowManager.loadCharts(sortByType.toOrder()) + } + } + + private fun startQuotesUpdateTimer() { + quotesUpdateJob?.cancel() + quotesUpdateJob = modelScope.launch { + while (true) { + delay(DELAY_TO_FETCH_QUOTES) + isVisibleOnScreen.first { it } + marketsBatchFlowManager.updateQuotes() + } + } + } + + private fun updateCallbacks() { + state.update { feedListUM -> + feedListUM.copy( + searchBar = state.value.searchBar.copy(onQueryChange = searchBarStateFactory::onSearchQueryChange), + feedListCallbacks = feedListUM.feedListCallbacks.copy( + onSortTypeClick = ::onSortTypeClick, + onMarketItemClick = { item -> + val tokenMarket = marketsBatchFlowManager.getTokenMarketById(item.id) + if (tokenMarket != null) { + params.feedClickIntents.onMarketItemClick( + token = tokenMarket.toSerializableParam(), + appCurrency = currentAppCurrency.value, + ) + } + }, + onMarketOpenClick = { sortBy -> + params.feedClickIntents.onMarketOpenClick(sortBy) + }, + onArticleClick = { articleId -> + params.feedClickIntents.onArticleClick(articleId) + }, + onOpenAllNews = { + params.feedClickIntents.onOpenAllNews() + }, + ), + ) + } + } + + private fun SortByTypeUM.toOrder(): TokenMarketListConfig.Order { + return when (this) { + SortByTypeUM.Rating -> TokenMarketListConfig.Order.ByRating + SortByTypeUM.Trending -> TokenMarketListConfig.Order.Trending + SortByTypeUM.ExperiencedBuyers -> TokenMarketListConfig.Order.Buyers + SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers + SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers + SortByTypeUM.Staking -> TokenMarketListConfig.Order.Staking + SortByTypeUM.YieldSupply -> TokenMarketListConfig.Order.YieldSupply + } + } + + companion object { + private const val DELAY_TO_FETCH_QUOTES = 60_000L + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt new file mode 100644 index 0000000000..7e518f9c26 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt @@ -0,0 +1,15 @@ +package com.tangem.features.feed.model.feed + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketParams +import com.tangem.features.feed.ui.market.state.SortByTypeUM + +/** + * Callback interface for feed model navigation actions. + */ +internal interface FeedModelClickIntents { + fun onMarketItemClick(token: TokenMarketParams, appCurrency: AppCurrency) + fun onMarketOpenClick(sortBy: SortByTypeUM) + fun onArticleClick(articleId: Int) + fun onOpenAllNews() +} \ 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..c35da92100 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 @@ -1,9 +1,6 @@ package com.tangem.features.feed.ui.feed -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith +import androidx.compose.animation.* import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* @@ -14,7 +11,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -22,25 +21,29 @@ 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.markets.MarketsListItem +import com.tangem.common.ui.markets.MarketsListItemPlaceholder +import com.tangem.common.ui.markets.models.MarketsListItemUM +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.UnableToLoadData 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,19 +51,60 @@ 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.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.feed.state.* 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 FeedList(state: FeedListUM, modifier: Modifier = Modifier) { + val background = LocalMainBottomSheetColor.current.value + + AnimatedContent( + modifier = modifier, + targetState = state.globalState, + ) { animatedState -> + when (animatedState) { + is GlobalFeedState.Loading -> { + FeeListLoading( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .drawBehind { drawRect(background) }, + ) + } + is GlobalFeedState.Error -> { + FeedListGlobalError( + onRetryClick = animatedState.onRetryClicked, + modifier = Modifier.drawBehind { drawRect(background) }, + ) + } + is GlobalFeedState.Content -> { + FeeListContent( + modifier = Modifier, + state = state, + ) + } + } + } +} + +@Composable +private fun FeeListContent(state: FeedListUM, modifier: Modifier = Modifier) { val background = LocalMainBottomSheetColor.current.value Column( modifier = modifier @@ -68,67 +112,61 @@ internal fun FeedList(state: FeedListUM, onHeaderSizeChange: (Dp) -> Unit, modif .verticalScroll(rememberScrollState()) .drawBehind { drawRect(background) }, ) { - SearchBar( + Column( 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()) - } - } + .fillMaxSize() + .padding(WindowInsets.navigationBars.asPaddingValues()), + ) { + SpacerH(20.dp) + + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + text = stringResourceSafe(R.string.feed_market_and_news), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + text = state.currentDate, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.tertiary, + ) + + SpacerH(32.dp) + + AnimatedVisibility(state.marketChartConfig.marketCharts[SortByTypeUM.Rating] != null) { + val marketChart = remember(state.marketChartConfig.marketCharts[SortByTypeUM.Rating]) { + state.marketChartConfig.marketCharts[SortByTypeUM.Rating] } - .padding(bottom = 4.dp), - state = state.searchBar, - ) + if (marketChart != null) { + MarketBlock( + marketChart = marketChart, + feedListCallbacks = state.feedListCallbacks, + ) + } + } - SpacerH(20.dp) + NewsBlock( + news = state.news, + feedListCallbacks = state.feedListCallbacks, + trendingArticle = state.trendingArticle, + ) - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - text = stringResourceSafe(R.string.feed_market_and_news), - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - ) - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - text = state.currentDate, - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.tertiary, - ) - - SpacerH(32.dp) - - MarketBlock( - marketChartConfig = state.marketChartConfig, - feedListCallbacks = state.feedListCallbacks, - ) - - NewsBlock( - news = state.news, - feedListCallbacks = state.feedListCallbacks, - trendingArticle = state.trendingArticle, - ) - - MarketPulseBlock( - marketChartConfig = state.marketChartConfig, - feedListCallbacks = state.feedListCallbacks, - ) + MarketPulseBlock( + marketChartConfig = state.marketChartConfig, + feedListCallbacks = state.feedListCallbacks, + ) + } } } @Composable -private fun MarketBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) { - if (marketChartConfig.marketCharts.isNotEmpty()) { +private fun MarketBlock(marketChart: MarketChartUM, feedListCallbacks: FeedListCallbacks) { + Column(modifier = Modifier.fillMaxWidth()) { Header( title = { Text( @@ -142,20 +180,33 @@ private fun MarketBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: SpacerH(12.dp) - marketChartConfig.marketCharts[SortByTypeUM.Rating]?.let { chart -> - Charts( - onItemClick = feedListCallbacks.onMarketItemClick, - modifier = Modifier.padding(horizontal = 16.dp), - marketChart = chart, - ) - } + Charts( + onItemClick = feedListCallbacks.onMarketItemClick, + modifier = Modifier.padding(horizontal = 16.dp), + marketChart = marketChart, + ) + SpacerH(32.dp) } } @Composable private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) { + val onSeeAllClick by rememberUpdatedState { + feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) + } if (marketChartConfig.marketCharts.isNotEmpty()) { + Header( + title = { + Text( + text = stringResourceSafe(R.string.markets_pulse_common_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + }, + onSeeAllClick = { onSeeAllClick() }, + ) + LazyRow( modifier = Modifier.padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, @@ -175,17 +226,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 +247,41 @@ 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() + } + is NewsUM.Error -> { + NewsErrorBlock(onRetryClick = newsUM.onRetryClicked) + } + } + } +} + +@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 +307,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 +322,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 +334,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 +357,7 @@ private fun Header(title: @Composable () -> Unit, onSeeAllClick: () -> Unit) { .fillMaxWidth() .padding(horizontal = 20.dp), horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, ) { title() @@ -310,12 +376,11 @@ private fun Charts( onItemClick: (MarketsListItemUM) -> Unit, modifier: Modifier = Modifier, ) { - BlockCard(modifier) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - ) { + BlockCard( + modifier = modifier, + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) { + Column(modifier = Modifier.fillMaxWidth()) { when (marketChart) { MarketChartUM.Loading -> { repeat(DEFAULT_CHART_SIZE_IN_MARKET) { @@ -323,7 +388,10 @@ private fun Charts( } } is MarketChartUM.LoadingError -> { - // TODO will be created in [REDACTED_TASK_KEY] + UnableToLoadData( + onRetryClick = marketChart.onRetryClicked, + modifier = Modifier.fillMaxWidth(), + ) } is MarketChartUM.Content -> { marketChart.items.fastForEach { chart -> @@ -370,6 +438,43 @@ private fun FilterChip(sortByTypeUM: SortByTypeUM, isSelected: Boolean, onClick: } } +@Composable +private fun FeedListGlobalError(onRetryClick: () -> Unit, modifier: Modifier = Modifier) { + val background = LocalMainBottomSheetColor.current.value + Box( + modifier = modifier + .fillMaxSize() + .drawBehind { drawRect(background) } + .padding(16.dp), + contentAlignment = Alignment.Center, + ) { + UnableToLoadData(onRetryClick = onRetryClick) + } +} + +@Composable +private fun NewsErrorBlock(onRetryClick: () -> Unit) { + Column { + Header( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResourceSafe(R.string.common_news), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + } + }, + onSeeAllClick = {}, + ) + SpacerH(12.dp) + UnableToLoadData( + onRetryClick = onRetryClick, + modifier = Modifier.fillMaxWidth(), + ) + } +} + private const val DEFAULT_CHART_SIZE_IN_MARKET = 5 private const val GRADIENT_START = 0f private const val GRADIENT_END = 0.5f @@ -380,9 +485,6 @@ private val LinearGradientSecondPart = Color(0xFFE05AED) @Composable private fun FeedListPreview() { TangemThemePreview { - FeedList( - state = createFeedPreviewState(), - onHeaderSizeChange = {}, - ) + FeedList(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..2e24ff3ec2 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt @@ -0,0 +1,122 @@ +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.markets.MarketsListItemPlaceholder +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 + +@Composable +internal fun FeeListLoading(modifier: Modifier = Modifier) { + Column(modifier) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(WindowInsets.navigationBars.asPaddingValues()), + ) { + MarketLoadingBlock() + NewsLoadingBlock() + MarketPulseLoadingBlock() + } + } +} + +@Composable +internal fun MarketLoadingBlock() { + RectangleShimmer( + modifier = Modifier + .padding(start = 16.dp) + .size(width = 104.dp, height = 18.dp), + ) + SpacerH(15.dp) + ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp)) + SpacerH(35.dp) +} + +@Composable +internal fun MarketPulseLoadingBlock() { + RectangleShimmer( + modifier = Modifier + .padding(start = 16.dp) + .size(width = 104.dp, height = 18.dp), + ) + SpacerH(15.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(16.dp) + ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp)) + SpacerH(32.dp) +} + +@Composable +internal fun NewsLoadingBlock() { + Column { + RectangleShimmer( + modifier = Modifier + .padding(start = 16.dp) + .size(width = 104.dp, height = 18.dp), + ) + SpacerH(15.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() + } + } + SpacerH(35.dp) + } +} + +@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 { + MarketLoadingBlock() + NewsLoadingBlock() + 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..9cf0dea267 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,17 +1,16 @@ 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.markets.models.MarketsListItemUM 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.event.consumedEvent +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.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.feed.ui.feed.state.* -import com.tangem.features.feed.ui.market.state.MarketsListItemUM import com.tangem.features.feed.ui.market.state.SortByTypeUM import kotlinx.collections.immutable.* @@ -38,7 +37,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), @@ -94,7 +93,6 @@ internal object FeedListPreviewDataProvider { ): MarketChartUM.Content { return MarketChartUM.Content( items = items, - triggerScrollReset = consumedEvent(), sortChartConfig = SortChartConfigUM( sortByType = sortByType, isSelected = isSelected, @@ -107,7 +105,7 @@ internal object FeedListPreviewDataProvider { id = 1, title = "Bitcoin ETF reaches new highs, institutions pile in", score = 0.82f, - createdAt = "2h ago", + createdAt = TextReference.Str("Yesterday"), isTrending = true, tags = createArticleTags(), isViewed = false, @@ -116,7 +114,7 @@ internal object FeedListPreviewDataProvider { id = 2, title = "Layer 2 networks battle for dominance amid fee wars", score = 0.71f, - createdAt = "4h ago", + createdAt = TextReference.Str("Yesterday"), isTrending = false, tags = createArticleTags(), isViewed = true, @@ -125,7 +123,7 @@ internal object FeedListPreviewDataProvider { id = 3, title = "Stablecoins expand on-ramps across LATAM", score = 0.65f, - createdAt = "Yesterday", + createdAt = TextReference.Str("Yesterday"), isTrending = false, tags = createArticleTags(), isViewed = false, @@ -134,7 +132,7 @@ internal object FeedListPreviewDataProvider { id = 4, title = "Stablecoins expand on-ramps across LATAM", score = 0.65f, - createdAt = "Yesterday", + createdAt = TextReference.Str("Yesterday"), isTrending = false, tags = createArticleTags(), isViewed = false, @@ -143,7 +141,7 @@ internal object FeedListPreviewDataProvider { id = 5, title = "Stablecoins expand on-ramps across LATAM", score = 0.65f, - createdAt = "Yesterday", + createdAt = TextReference.Str("Yesterday"), isTrending = false, tags = createArticleTags(), isViewed = false, @@ -152,28 +150,25 @@ internal object FeedListPreviewDataProvider { id = 6, title = "Stablecoins expand on-ramps across LATAM", score = 0.65f, - createdAt = "Yesterday", + createdAt = TextReference.Str("Yesterday"), isTrending = false, tags = createArticleTags(), isViewed = false, ), ) - 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..3b613ccc86 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 @@ -1,10 +1,9 @@ package com.tangem.features.feed.ui.feed.state import androidx.compose.runtime.Immutable +import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.common.ui.news.ArticleConfigUM import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.core.ui.event.StateEvent -import com.tangem.features.feed.ui.market.state.MarketsListItemUM import com.tangem.features.feed.ui.market.state.SortByTypeUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap @@ -14,9 +13,10 @@ 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, + val globalState: GlobalFeedState = GlobalFeedState.Content, ) internal data class FeedListCallbacks( @@ -28,6 +28,13 @@ internal data class FeedListCallbacks( val onSortTypeClick: (SortByTypeUM) -> Unit, ) +@Immutable +internal sealed interface NewsUM { + data object Loading : NewsUM + data class Content(val content: ImmutableList) : NewsUM + data class Error(val onRetryClicked: () -> Unit) : NewsUM +} + internal data class MarketChartConfig( val marketCharts: ImmutableMap, val currentSortByType: SortByTypeUM = SortByTypeUM.TopGainers, @@ -40,7 +47,6 @@ internal sealed interface MarketChartUM { data class Content( val items: ImmutableList, - val triggerScrollReset: StateEvent, val sortChartConfig: SortChartConfigUM, ) : MarketChartUM @@ -49,7 +55,14 @@ 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 +) + +@Immutable +internal sealed interface GlobalFeedState { + data object Loading : GlobalFeedState + data object Content : GlobalFeedState + data class Error(val onRetryClicked: () -> Unit) : GlobalFeedState +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt new file mode 100644 index 0000000000..b053465040 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedMarketsBatchFlowManager.kt @@ -0,0 +1,392 @@ +package com.tangem.features.feed.ui.feed.state + +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.* +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.feed.model.converter.MarketsTokenItemConverter +import com.tangem.features.feed.ui.market.state.MarketsListUM +import com.tangem.features.feed.ui.market.state.SortByTypeUM +import com.tangem.pagination.Batch +import com.tangem.pagination.BatchAction +import com.tangem.pagination.BatchFetchResult +import com.tangem.pagination.PaginationStatus +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +@Suppress("LongParameterList") +internal class FeedMarketsBatchFlowManager( + private val getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase, + private val currentAppCurrency: Provider, + private val modelScope: CoroutineScope, + private val dispatchers: CoroutineDispatcherProvider, +) { + private val managersByOrder = TokenMarketListConfig.Order.entries.associateWith { order -> + createManagerForOrder(order) + } + + val itemsByOrder: StateFlow>> = + combine( + TokenMarketListConfig.Order.entries.mapNotNull { order -> + managersByOrder[order]?.uiItems?.map { items -> order to items } + }, + ) { itemsList -> + itemsList.associate { (order, items) -> + val sortByType = order.toSortByTypeUM() + sortByType to items + } + }.stateIn( + scope = modelScope, + started = SharingStarted.Companion.Eagerly, + initialValue = emptyMap(), + ) + + val loadingStatesByOrder: StateFlow> = + combine( + TokenMarketListConfig.Order.entries.mapNotNull { order -> + managersByOrder[order]?.isLoading?.map { isLoading -> order to isLoading } + }, + ) { loadingStatesList -> + loadingStatesList.associate { (order, isLoading) -> + val sortByType = order.toSortByTypeUM() + sortByType to isLoading + } + }.stateIn( + scope = modelScope, + started = SharingStarted.Companion.Eagerly, + initialValue = emptyMap(), + ) + + val errorStatesByOrder: StateFlow> = + combine( + TokenMarketListConfig.Order.entries.mapNotNull { order -> + managersByOrder[order]?.hasError?.map { hasError -> order to hasError } + }, + ) { errorStatesList -> + errorStatesList.associate { (order, hasError) -> + val sortByType = order.toSortByTypeUM() + sortByType to hasError + } + }.stateIn( + scope = modelScope, + started = SharingStarted.Companion.Eagerly, + initialValue = emptyMap(), + ) + + init { + managersByOrder.values.forEach { manager -> + manager.reload(currentAppCurrency().code) + } + } + + private fun createManagerForOrder(order: TokenMarketListConfig.Order): SingleOrderManager { + val actionsFlow = MutableSharedFlow>() + + val batchFlow = getTopFiveMarketTokenUseCase( + batchingContext = TokenListBatchingContext( + actionsFlow = actionsFlow, + coroutineScope = modelScope, + ), + order = order, + ) + + return SingleOrderManager( + order = order, + actionsFlow = actionsFlow, + batchFlow = batchFlow, + currentAppCurrency = currentAppCurrency, + modelScope = modelScope, + dispatchers = dispatchers, + ) + } + + fun reloadAll() { + managersByOrder.values.forEach { manager -> + manager.reload(currentAppCurrency().code) + } + } + + fun updateQuotes() { + managersByOrder.values.forEach { manager -> + manager.updateQuotes(currentAppCurrency().code) + } + } + + fun loadCharts(order: TokenMarketListConfig.Order) { + managersByOrder[order]?.loadCharts() + } + + fun getOnLastBatchLoadedSuccessFlow(order: TokenMarketListConfig.Order): Flow? { + return managersByOrder[order]?.onLastBatchLoadedSuccess + } + + fun getTokenMarketById(tokenId: CryptoCurrency.RawID): TokenMarket? { + return managersByOrder.values + .firstNotNullOfOrNull { manager -> manager.getTokenMarketById(tokenId) } + } + + private class SingleOrderManager( + val order: TokenMarketListConfig.Order, + private val actionsFlow: MutableSharedFlow>, + private val batchFlow: TokenListBatchFlow, + private val currentAppCurrency: Provider, + private val modelScope: CoroutineScope, + private val dispatchers: CoroutineDispatcherProvider, + ) { + private val updateStateJob = JobHolder() + private val resultBatches = MutableStateFlow(ResultBatches()) + private val uiBatches = resultBatches.map { it.uiBatches } + + val uiItems: StateFlow> = + uiBatches + .map { batches -> + batches.asSequence() + .map { it.data } + .flatten() + .toImmutableList() + } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Companion.Eagerly, + initialValue = persistentListOf(), + ) + + val isLoading = batchFlow.state + .map { state -> + when (state.status) { + is PaginationStatus.InitialLoading -> true + is PaginationStatus.NextBatchLoading -> true + else -> false + } + } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Companion.Eagerly, + initialValue = false, + ) + + val hasError = batchFlow.state + .map { state -> + when (val status = state.status) { + is PaginationStatus.InitialLoadingError -> true + is PaginationStatus.Paginating -> status.lastResult is BatchFetchResult.Error + else -> false + } + } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Companion.Eagerly, + initialValue = false, + ) + + val onLastBatchLoadedSuccess = batchFlow.state + .distinctUntilChanged { old, new -> old.status == new.status && old.data.size == new.data.size } + .mapNotNull { batchListState -> + when (val status = batchListState.status) { + is PaginationStatus.Paginating -> { + if (status.lastResult is BatchFetchResult.Success) { + batchListState.data.lastOrNull()?.key + } else { + null + } + } + is PaginationStatus.EndOfPagination -> { + batchListState.data.lastOrNull()?.key + } + else -> null + } + } + + init { + batchFlow.state + .map { it.data } + .distinctUntilChanged { a, b -> + a.size == b.size && + a.map { it.key } == b.map { it.key } && + a.map { it.data }.flatten() == b.map { it.data }.flatten() + } + .onEach { + coroutineScope { + launch { + updateState(it) + }.saveIn(updateStateJob) + } + } + .flowOn(dispatchers.default) + .launchIn(modelScope) + } + + private suspend fun updateState(newList: List>>, forceUpdate: Boolean = false) = + withContext(dispatchers.default) { + resultBatches.update { resultBatches -> + val items = resultBatches.uiBatches + val previousList = resultBatches.processedItems + + val converter = MarketsTokenItemConverter( + currentTrendInterval = MarketsListUM.TrendInterval.H24, + appCurrency = currentAppCurrency(), + ) + + if (newList.isEmpty()) { + return@update ResultBatches(processedItems = emptyList()) + } + + val isInitialLoading = + forceUpdate || previousList.isNullOrEmpty() || newList.first().key != previousList.first().key + + val outItems = if (isInitialLoading) { + newList.map { batch -> + Batch( + key = batch.key, + data = converter.convertList(batch.data), + ) + } + } else { + // As nextBatchSize = 0, we only have one batch, but keep the logic for safety + if (previousList.size != newList.size) { + val keysToAdd = newList.map { it.key }.subtract(previousList.map { it.key }.toSet()) + val newBatches = newList.filter { keysToAdd.contains(it.key) } + + items + newBatches.map { batch -> + Batch( + key = batch.key, + data = converter.convertList(batch.data), + ) + } + } else { + items.mapIndexed { batchIndex, batch -> + val prevBatch = previousList[batchIndex] + val newBatch = newList[batchIndex] + if (prevBatch == newBatch) return@mapIndexed batch + + Batch( + key = batch.key, + data = batch.data.mapIndexed { index, marketsListItemUM -> + val prevItem = prevBatch.data.getOrNull(index) + val newItem = newBatch.data.getOrNull(index) + if (prevItem != null && newItem != null) { + converter.update(prevItem, marketsListItemUM, newItem) + } else { + newItem?.let { converter.convert(it) } ?: marketsListItemUM + } + }, + ) + } + } + } + + currentCoroutineContext().ensureActive() + + ResultBatches( + uiBatches = outItems, + processedItems = newList, + ) + } + } + + fun reload(fiatPriceCurrency: String) { + modelScope.launch(dispatchers.default) { + resultBatches.value = ResultBatches() + actionsFlow.emit( + BatchAction.Reload( + requestParams = TokenMarketListConfig( + fiatPriceCurrency = fiatPriceCurrency, + searchText = null, + priceChangeInterval = TokenMarketListConfig.Interval.H24, + order = order, + ), + ), + ) + } + } + + fun updateQuotes(fiatPriceCurrency: String) { + modelScope.launch(dispatchers.default) { + actionsFlow.emit( + BatchAction.CancelUpdates { + it.updateRequest is TokenMarketUpdateRequest.UpdateQuotes + }, + ) + + actionsFlow.emit( + BatchAction.UpdateBatches( + keys = batchFlow + .state + .value + .data + .map { it.key } + .toSet(), + updateRequest = TokenMarketUpdateRequest.UpdateQuotes( + currencyId = fiatPriceCurrency, + ), + async = true, + operationId = "update quotes", + ), + ) + } + } + + fun loadCharts() { + modelScope.launch(dispatchers.default) { + val currentData = batchFlow.state.value.data + val alreadyLoadedChartsBatchKeys = currentData + .filter { batch -> + val first = batch.data.firstOrNull() ?: return@filter false + first.tokenCharts.h24 != null + } + .map { it.key } + .toSet() + + val batchesKeysToLoad = currentData.map { it.key }.toSet().minus(alreadyLoadedChartsBatchKeys) + + if (batchesKeysToLoad.isNotEmpty()) { + actionsFlow.emit( + BatchAction.UpdateBatches( + keys = batchesKeysToLoad, + updateRequest = TokenMarketUpdateRequest.UpdateChart( + interval = TokenMarketListConfig.Interval.H24, + currency = currentAppCurrency().code, + ), + async = true, + operationId = batchesKeysToLoad.toString() + "h24", + ), + ) + } + } + } + + fun getTokenMarketById(tokenId: CryptoCurrency.RawID): TokenMarket? { + return resultBatches.value.processedItems + ?.asSequence() + ?.flatMap { it.data } + ?.firstOrNull { it.id == tokenId } + } + + private data class ResultBatches( + val uiBatches: List>> = emptyList(), + val processedItems: List>>? = null, + ) + } + + private fun TokenMarketListConfig.Order.toSortByTypeUM(): SortByTypeUM { + return when (this) { + TokenMarketListConfig.Order.ByRating -> SortByTypeUM.Rating + TokenMarketListConfig.Order.Trending -> SortByTypeUM.Trending + TokenMarketListConfig.Order.Buyers -> SortByTypeUM.ExperiencedBuyers + TokenMarketListConfig.Order.TopGainers -> SortByTypeUM.TopGainers + TokenMarketListConfig.Order.TopLosers -> SortByTypeUM.TopLosers + TokenMarketListConfig.Order.Staking -> SortByTypeUM.Staking + TokenMarketListConfig.Order.YieldSupply -> SortByTypeUM.YieldSupply + } + } +} \ 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..15aee2ff6a --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt @@ -0,0 +1,128 @@ +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.core.ui.extensions.WrappedList +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.FormattedDate +import com.tangem.core.ui.utils.getFormattedDate +import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.news.ShortArticle +import com.tangem.domain.models.news.TrendingNews +import com.tangem.features.feed.impl.R +import com.tangem.utils.Provider +import com.tangem.utils.StringsSigns +import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toPersistentSet +import org.joda.time.DateTime + +internal class TrendingNewsStateFactory( + private val currentStateProvider: Provider, + private val onStateUpdate: (FeedListUM) -> Unit, +) { + + fun updateTrendingNewsState(result: TrendingNews, onRetryClicked: () -> Unit) { + val currentState = currentStateProvider() + when (result) { + is TrendingNews.Data -> handleDataState(currentState, result.articles) + is TrendingNews.Error -> handleErrorState(currentState, onRetryClicked) + } + } + + private fun handleDataState(currentState: FeedListUM, articles: List) { + val (trendingArticle, commonArticles) = separateTrendingAndCommonArticles(articles) + + onStateUpdate( + currentState.copy( + trendingArticle = trendingArticle?.let { mapToArticleConfigUM(it, isTrending = true) }, + news = NewsUM.Content( + commonArticles.map { mapToArticleConfigUM(it, isTrending = false) }.toPersistentList(), + ), + ), + ) + } + + private fun handleErrorState(currentState: FeedListUM, onRetryClicked: () -> Unit) { + onStateUpdate( + currentState.copy( + trendingArticle = null, + news = NewsUM.Error(onRetryClicked = onRetryClicked), + ), + ) + } + + private fun separateTrendingAndCommonArticles( + articles: List, + ): Pair> { + val trendingArticleIndex = articles.indexOfFirst { it.isTrending } + return if (trendingArticleIndex != -1) { + val trendingArticle = articles[trendingArticleIndex] + val commonArticles = articles.toMutableList().apply { removeAt(trendingArticleIndex) } + trendingArticle to commonArticles + } else { + null to articles + } + } + + private fun mapToArticleConfigUM(article: ShortArticle, isTrending: Boolean): ArticleConfigUM { + return ArticleConfigUM( + id = article.id, + title = article.title, + score = article.score, + isTrending = isTrending, + tags = buildArticleTags(article), + createdAt = mapFormattedDate(article.createdAt), + isViewed = article.viewed, + ) + } + + private fun buildArticleTags(article: ShortArticle): kotlinx.collections.immutable.ImmutableSet { + val categoryLabels = article.categories.map { category -> + LabelUM(text = TextReference.Str(category.name)) + } + val tokenLabels = article.relatedTokens.map { token -> + LabelUM( + text = TextReference.Str(token.symbol), + leadingContent = LabelLeadingContentUM.Token( + iconUrl = getTokenIconUrlFromDefaultHost( + tokenId = CryptoCurrency.RawID(token.id), + ), + ), + ) + } + return (categoryLabels + tokenLabels).toPersistentSet() + } + + private fun mapFormattedDate(createdAt: String): TextReference { + val formattedDate = getFormattedDate( + createdAt = createdAt, + now = DateTime.now(), + ) + return when (formattedDate) { + is FormattedDate.FullDate -> TextReference.Str(value = formattedDate.date) + is FormattedDate.HoursAgo -> TextReference.PluralRes( + id = R.plurals.news_published_hours_ago, + count = formattedDate.hours, + formatArgs = wrappedList(formattedDate.hours), + ) + is FormattedDate.MinutesAgo -> TextReference.PluralRes( + id = R.plurals.news_published_minutes_ago, + count = formattedDate.minutes, + formatArgs = wrappedList(formattedDate.minutes), + ) + is FormattedDate.Today -> TextReference.Combined( + refs = WrappedList( + data = listOf( + TextReference.Res(R.string.common_today), + TextReference.Str(StringsSigns.COMA_SIGN), + TextReference.Str(StringsSigns.WHITE_SPACE), + TextReference.Str(formattedDate.time), + ), + ), + ) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListUM.kt index ea7e5f740a..88dc1834e7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/state/MarketsListUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.feed.ui.market.state import androidx.compose.runtime.Immutable +import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.fields.entity.SearchBarUM @@ -40,6 +41,7 @@ enum class SortByTypeUM(val text: TextReference) { TopGainers(resourceReference(R.string.markets_sort_by_top_gainers_title)), TopLosers(resourceReference(R.string.markets_sort_by_top_losers_title)), Staking(resourceReference(R.string.common_staking)), + YieldSupply(resourceReference(R.string.markets_sort_by_yield_mode_title)), } @Immutable 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/AccessCodeComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt index ec2f7d3ca4..08693225cc 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt @@ -2,12 +2,16 @@ package com.tangem.features.hotwallet.accesscode import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.platform.LocalFocusManager import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.essenty.lifecycle.doOnResume import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect import com.tangem.features.hotwallet.accesscode.ui.AccessCode import com.tangem.domain.models.wallet.UserWalletId @@ -31,10 +35,18 @@ internal class AccessCodeComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + + EventEffect(event = state.requestFocus) { + focusManager.clearFocus() + focusRequester.requestFocus() + } DisableScreenshotsDisposableEffect() AccessCode( modifier = modifier, + focusRequester = focusRequester, state = state, ) } 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..f08755fcab 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 @@ -7,10 +7,13 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.components.fields.PinTextColor +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent 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 +31,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 +59,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 +121,56 @@ 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( + accessCode = "", + onAccessCodeChange = ::onAccessCodeChange, + requestFocus = triggeredEvent(Unit, ::consumeRequestFocusEvent), + ) + } + }, + ), + secondAction = EventMessageAction( + title = resourceReference(R.string.access_code_alert_validation_ok), + onClick = ::setNewCode, + ), + isDismissable = false, + ), + ) + } + + private fun consumeRequestFocusEvent() { + uiState.update { currentState -> + currentState.copy(requestFocus = consumedEvent()) + } + } + private suspend fun showErrorAndReset() { uiState.update { currentState -> currentState.copy( @@ -152,11 +195,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 +217,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/accesscode/entity/AccessCodeUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt index 1bc94c1d7c..978071789b 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt @@ -1,6 +1,8 @@ package com.tangem.features.hotwallet.accesscode.entity import com.tangem.core.ui.components.fields.PinTextColor +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH internal data class AccessCodeUM( @@ -8,6 +10,7 @@ internal data class AccessCodeUM( val accessCodeColor: PinTextColor, val onAccessCodeChange: (String) -> Unit, val isConfirmMode: Boolean, + val requestFocus: StateEvent = consumedEvent(), ) { val accessCodeLength: Int = ACCESS_CODE_LENGTH } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt index 5d0526fe6b..ecfa44d9d6 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt @@ -5,8 +5,10 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* 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.focus.FocusRequester import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -20,7 +22,11 @@ import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM @Suppress("LongParameterList", "LongMethod") @Composable -internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) { +internal fun AccessCode( + state: AccessCodeUM, + modifier: Modifier = Modifier, + focusRequester: FocusRequester = remember { FocusRequester() }, +) { Column( modifier = modifier .fillMaxSize() @@ -77,6 +83,7 @@ internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) { value = state.accessCode, pinTextColor = state.accessCodeColor, onValueChange = state.onAccessCodeChange, + focusRequester = focusRequester, ) } } 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..bcc2a2c440 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,8 +1,10 @@ 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.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text @@ -18,6 +20,7 @@ import com.tangem.core.ui.res.TangemTheme private const val DISABLED_COLORS_ALPHA = 0.5f +@OptIn(ExperimentalLayoutApi::class) @Suppress("LongParameterList") @Composable internal fun OptionBlock( @@ -43,11 +46,11 @@ internal fun OptionBlock( } .padding(16.dp), ) { - Row { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { Text( - modifier = Modifier - .weight(1f, fill = false) - .padding(end = 4.dp), 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/createhardwarewallet/ui/CreateHardwareWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/ui/CreateHardwareWalletContent.kt index 8f2ce41cfd..095790c30a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/ui/CreateHardwareWalletContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/ui/CreateHardwareWalletContent.kt @@ -3,6 +3,8 @@ package com.tangem.features.hotwallet.createhardwarewallet.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.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -42,6 +44,7 @@ internal fun CreateHardwareWalletContent(state: CreateHardwareWalletUM, modifier Column( modifier = Modifier .weight(1f) + .verticalScroll(rememberScrollState()) .padding( start = 16.dp, top = 24.dp, 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/createmobilewallet/ui/CreateMobileWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt index 99f127b9bf..e6916bdcfd 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt @@ -3,6 +3,8 @@ package com.tangem.features.hotwallet.createmobilewallet.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.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -41,6 +43,7 @@ internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Mo Column( modifier = Modifier .weight(1f) + .verticalScroll(rememberScrollState()) .padding( start = 16.dp, top = 24.dp, 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..8d2a207aa8 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,6 +3,8 @@ 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 @@ -23,7 +25,7 @@ import com.tangem.features.hotwallet.manualbackup.start.entity.ManualBackupStart @Composable internal fun ManualBackupStartContent(state: ManualBackupStartUM, modifier: Modifier = Modifier) { Column( - modifier = modifier + modifier .background(TangemTheme.colors.background.primary) .fillMaxSize() .padding( @@ -33,51 +35,58 @@ internal fun ManualBackupStartContent(state: ManualBackupStartUM, modifier: Modi bottom = 16.dp, ), ) { - Text( + Column( modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = 16.dp, - vertical = 8.dp, + .weight(1f) + .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 .fillMaxWidth() 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..3f86e09a9d 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 @@ -31,12 +31,6 @@ internal class HotWalletStepperModel @Inject constructor( } fun onSkipClick() { - // 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/upgradewallet/ui/UpgradeWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/ui/UpgradeWalletContent.kt index 515d01c482..a643ee75e0 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/ui/UpgradeWalletContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/ui/UpgradeWalletContent.kt @@ -3,6 +3,8 @@ package com.tangem.features.hotwallet.upgradewallet.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.Icon import androidx.compose.material3.Text @@ -44,6 +46,7 @@ internal fun UpgradeWalletContent(state: UpgradeWalletUM, modifier: Modifier = M Column( modifier = Modifier .weight(1f) + .verticalScroll(rememberScrollState()) .padding( start = 16.dp, top = 24.dp, 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..283d1c7eea 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 @@ -61,7 +62,7 @@ internal class WalletActivationModel @Inject constructor( val pushNotificationsCallbacks = PushNotificationsCallbacks() val mobileWalletSetupFinishedModelCallbacks = MobileWalletSetupFinishedModelCallbacks() - val isStartingWithAccessCode = params.isBackupExists + private val isStartingWithAccessCode = params.isBackupExists val stackNavigation = StackNavigation() val startRoute = if (isStartingWithAccessCode) { WalletActivationRoute.SetAccessCode @@ -70,18 +71,28 @@ 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, - ), - ) + when (startRoute) { + is WalletActivationRoute.ManualBackupStart -> { + analyticsEventHandler.send( + event = WalletSettingsAnalyticEvents.RecoveryPhraseScreenInfo( + source = analyticsSource.value, + action = analyticsAction.value, + ), + ) + } + is WalletActivationRoute.SetAccessCode -> { + analyticsEventHandler.send( + event = WalletSettingsAnalyticEvents.AccessCodeScreenOpened( + source = analyticsSource.value, + ), + ) + } + else -> Unit } } @@ -134,7 +145,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 +171,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 +183,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 +195,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 +204,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 +215,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/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/ui/WalletHardwareBackupContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/ui/WalletHardwareBackupContent.kt index 9635f67b45..8e4eafbf78 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/ui/WalletHardwareBackupContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/ui/WalletHardwareBackupContent.kt @@ -117,6 +117,8 @@ private fun PurchaseBlock(onBuyClick: () -> Unit, modifier: Modifier = Modifier) text = stringResourceSafe(R.string.wallet_add_hardware_purchase), style = TangemTheme.typography.button, color = TangemTheme.colors.text.primary1, + maxLines = 3, + overflow = TextOverflow.Ellipsis, ) SecondaryButton( 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/build.gradle.kts b/features/markets/impl/build.gradle.kts index ac6adcc8fb..24d38f3109 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -45,6 +45,8 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.notifications.models) implementation(projects.domain.transaction) + implementation(projects.domain.yieldSupply.models) + implementation(projects.domain.yieldSupply) // FIXME [REDACTED_TASK_KEY] // Remove the "Buy" and "Sell" actions from the redux middleware. 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..a547351b4c 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( @@ -39,6 +47,7 @@ internal class PortfolioAnalyticsEvent( TokenActionsBSContentUM.Action.Receive -> "Button - Receive" TokenActionsBSContentUM.Action.Exchange -> "Button - Swap" TokenActionsBSContentUM.Action.Stake -> "Button - Stake" + TokenActionsBSContentUM.Action.YieldMode -> "Button - Yield Mode" else -> "error" }, params = buildMap { @@ -47,5 +56,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/loader/PortfolioDataLoader.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt index ff72822f09..d5522e7218 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt @@ -1,5 +1,6 @@ package com.tangem.features.markets.portfolio.impl.loader +import arrow.core.getOrElse import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.core.lce.Lce @@ -11,6 +12,8 @@ import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.yield.supply.models.YieldSupplyAvailability +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetAvailabilityUseCase import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import javax.inject.Inject @@ -30,6 +33,7 @@ internal class PortfolioDataLoader @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, + private val yieldSupplyGetAvailabilityUseCase: YieldSupplyGetAvailabilityUseCase, private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, ) { @@ -72,7 +76,10 @@ internal class PortfolioDataLoader @Inject constructor( .flatMapLatest { walletsWithStatuses -> val actionsFlows = walletsWithStatuses.flatMap { (wallet, statuses) -> statuses.map { status -> - getCryptoCurrencyActionsUseCase(wallet, status) + val yieldSupplyAvailability = yieldSupplyGetAvailabilityUseCase(status.currency).getOrElse { + YieldSupplyAvailability.Unavailable + } + getCryptoCurrencyActionsUseCase(wallet, status, yieldSupplyAvailability) .map { PortfolioData.CryptoCurrencyData( userWallet = wallet, 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/portfolio/impl/model/PortfolioTokenUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt index ce68f5550d..b64ee6bf1f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt @@ -71,8 +71,8 @@ internal class PortfolioTokenUMConverter( ): PortfolioTokenUM.QuickActions { return PortfolioTokenUM.QuickActions( actions = toQuickActions(cryptoData.actions), - onQuickActionClick = { - when (it) { + onQuickActionClick = { quickActionUM -> + when (quickActionUM) { QuickActionUM.Buy -> tokenActionsHandler.handle( action = TokenActionsBSContentUM.Action.Buy, cryptoCurrencyData = cryptoData, @@ -89,6 +89,10 @@ internal class PortfolioTokenUMConverter( action = TokenActionsBSContentUM.Action.Stake, cryptoCurrencyData = cryptoData, ) + is QuickActionUM.YieldMode -> tokenActionsHandler.handle( + action = TokenActionsBSContentUM.Action.YieldMode, + cryptoCurrencyData = cryptoData, + ) } }, onQuickActionLongClick = { @@ -110,6 +114,7 @@ internal class PortfolioTokenUMConverter( is TokenActionsState.ActionState.Swap -> QuickActionUM.Exchange(showBadge = action.showBadge) is TokenActionsState.ActionState.Receive -> QuickActionUM.Receive is TokenActionsState.ActionState.Stake -> QuickActionUM.Stake + is TokenActionsState.ActionState.YieldMode -> QuickActionUM.YieldMode(apy = action.apy) else -> null }?.let(::add) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index c8f42a8843..7561518e9f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -15,11 +15,11 @@ import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.TokenActionsState -import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM @@ -65,6 +65,7 @@ internal class TokenActionsHandler @AssistedInject constructor( TokenActionsBSContentUM.Action.Sell -> onSellClick(cryptoCurrencyData) TokenActionsBSContentUM.Action.Send -> onSendClick(cryptoCurrencyData) TokenActionsBSContentUM.Action.Stake -> onStakeClick(cryptoCurrencyData) + TokenActionsBSContentUM.Action.YieldMode -> onYieldModeClick(cryptoCurrencyData) } } @@ -178,6 +179,32 @@ internal class TokenActionsHandler @AssistedInject constructor( ) } + private fun onYieldModeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + val yieldSupplyApy = cryptoCurrencyData.actions.filterIsInstance() + .firstOrNull()?.apy ?: return + + val (userWalletId, cryptoCurrencyStatus) = cryptoCurrencyData.let { currencyData -> + currencyData.userWallet.walletId to currencyData.status + } + if (cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true) { + router.push( + AppRoute.YieldSupplyActive( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + apy = yieldSupplyApy, + ), + ) + } else { + router.push( + AppRoute.YieldSupplyPromo( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + apy = yieldSupplyApy, + ), + ) + } + } + @AssistedFactory interface Factory { fun create( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt index ffb485d08c..e0987c16e7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt @@ -4,6 +4,7 @@ import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.features.markets.impl.R @Immutable @@ -39,4 +40,12 @@ internal sealed class QuickActionUM( description = resourceReference(R.string.stake_token_description), icon = R.drawable.ic_staking_24, ) + + data class YieldMode( + private val apy: String, + ) : QuickActionUM( + title = resourceReference(R.string.yield_module_start_earning), + description = resourceReference(R.string.yield_module_main_screen_promo_banner_message, wrappedList(apy)), + icon = R.drawable.ic_analytics_up_mini_24, + ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt index 83450b1ebc..078c25562f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt @@ -47,6 +47,10 @@ internal data class TokenActionsBSContentUM( text = resourceReference(R.string.common_stake), iconRes = R.drawable.ic_staking_24, ), + YieldMode( + text = resourceReference(R.string.yield_module_start_earning), + iconRes = R.drawable.ic_analytics_up_mini_24, + ), ; val order: Int = ordinal 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..996e95d31b 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, @@ -24,6 +24,7 @@ internal sealed class MarketsListAnalyticsEvent( SortByTypeUM.TopGainers -> "Gainers" SortByTypeUM.TopLosers -> "Losers" SortByTypeUM.Staking -> "Staking" + SortByTypeUM.YieldSupply -> "Yield Supply" }, "Period" to when (interval) { MarketsListUM.TrendInterval.H24 -> "24h" @@ -32,12 +33,11 @@ internal sealed class MarketsListAnalyticsEvent( }, ), ) + class YieldModePromoShown : MarketsListAnalyticsEvent(event = "Notice - Yield Mode Promo") - data object StakingPromoShown : MarketsListAnalyticsEvent(event = "Notice - Staking Promo") + class YieldModePromoClosed : MarketsListAnalyticsEvent(event = "Yield Mode Promo Closed") - data object StakingPromoClosed : MarketsListAnalyticsEvent(event = "Staking Promo Closed") - - data object StakingMoreInfoClicked : MarketsListAnalyticsEvent(event = "Staking More Info") + class YieldModeMoreInfoClicked : MarketsListAnalyticsEvent(event = "Yield Mode 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..e214f30658 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,23 +6,24 @@ 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 -import com.tangem.domain.markets.GetStakingNotificationMaxApyUseCase +import com.tangem.domain.markets.ShouldShowYieldModeMarketPromoUseCase import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.markets.TokenMarketListConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.promo.PromoRepository 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 import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM +import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM.TrendInterval import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -31,7 +32,6 @@ import com.tangem.utils.coroutines.saveIn import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import java.math.BigDecimal import javax.inject.Inject private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L @@ -45,7 +45,7 @@ internal class MarketsListModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - getStakingNotificationMaxApyUseCase: GetStakingNotificationMaxApyUseCase, + shouldShowYieldModeMarketPromoUseCase: ShouldShowYieldModeMarketPromoUseCase, private val promoRepository: PromoRepository, private val getUserCountryUseCase: GetUserCountryUseCase, private val analyticsEventHandler: AnalyticsEventHandler, @@ -69,9 +69,7 @@ internal class MarketsListModel @Inject constructor( visibleItemsChanged = { visibleItemIds.value = it }, onRetryButtonClicked = { activeListManager.reload() }, onTokenClick = { onTokenUIClicked(it) }, - onStakingNotificationClick = { analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingMoreInfoClicked) }, - onStakingNotificationCloseClick = { onStakingNotificationCloseClick() }, - onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens) }, + onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens()) }, ) private val mainMarketsListManager = MarketsListBatchFlowManager( @@ -112,53 +110,62 @@ internal class MarketsListModel @Inject constructor( marketsListUMStateManager.isInSearchStateFlow.flatMapLatest { isInSearchMode -> if (isInSearchMode) { combine( - searchMarketsListManager.uiItems, - searchMarketsListManager.isInInitialLoadingErrorState, - searchMarketsListManager.isSearchNotFoundState, - getStakingNotificationMaxApyUseCase(), - getUserCountryUseCase.invoke(), - ) { uiItems, isInInitialLoadingErrorState, isSearchNotFoundState, stakingMaxApy, userCountry -> + flow = searchMarketsListManager.uiItems, + flow2 = searchMarketsListManager.isInInitialLoadingErrorState, + flow3 = searchMarketsListManager.isSearchNotFoundState, + flow4 = shouldShowYieldModeMarketPromoUseCase( + appCurrency = currentAppCurrency.value, + interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(), + ), + flow5 = getUserCountryUseCase.invoke(), + ) { uiItems, isInInitialLoadingErrorState, isSearchNotFoundState, isYieldModePromo, userCountry -> MarketsItemsData( items = uiItems, isInErrorState = isInInitialLoadingErrorState, isSearchNotFound = isSearchNotFoundState, - stakingNotificationMaxApy = stakingMaxApy, + shouldShowYieldModePromo = isYieldModePromo, userCountry = userCountry, ) } } else { combine( - mainMarketsListManager.uiItems, - mainMarketsListManager.isInInitialLoadingErrorState, - getStakingNotificationMaxApyUseCase(), - getUserCountryUseCase.invoke(), - ) { uiItems, isInInitialLoadingErrorState, stakingNotificationMaxApy, userCountry -> + flow = mainMarketsListManager.uiItems, + flow2 = mainMarketsListManager.isInInitialLoadingErrorState, + flow3 = shouldShowYieldModeMarketPromoUseCase( + appCurrency = currentAppCurrency.value, + interval = marketsListUMStateManager.selectedInterval.toBatchRequestInterval(), + ), + flow4 = getUserCountryUseCase.invoke(), + ) { uiItems, isInInitialLoadingErrorState, shouldShowYieldModePromo, userCountry -> MarketsItemsData( items = uiItems, isInErrorState = isInInitialLoadingErrorState, isSearchNotFound = false, - stakingNotificationMaxApy = stakingNotificationMaxApy, + shouldShowYieldModePromo = shouldShowYieldModePromo, userCountry = userCountry, ) } } }.collect { marketsItemsData -> - val stakingNotificationMaxApy = marketsItemsData.stakingNotificationMaxApy?.takeUnless { - marketsItemsData.userCountry.getOrNull().needApplyFCARestrictions() - } - - if (marketsListUMStateManager.state.value.stakingNotificationMaxApy == null && - stakingNotificationMaxApy != null - ) { - analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoShown) + val shouldShowYieldModePromo = marketsItemsData.shouldShowYieldModePromo + if (marketsListUMStateManager.state.value.marketsNotificationUM == null && shouldShowYieldModePromo) { + analyticsEventHandler.send(MarketsListAnalyticsEvent.YieldModePromoShown()) } marketsListUMStateManager.onUiItemsChanged( uiItems = marketsItemsData.items, isInErrorState = marketsItemsData.isInErrorState, isSearchNotFound = marketsItemsData.isSearchNotFound, - stakingNotificationMaxApy = marketsItemsData.stakingNotificationMaxApy?.takeUnless { - marketsItemsData.userCountry.getOrNull().needApplyFCARestrictions() + marketsNotificationUM = if (shouldShowYieldModePromo) { + MarketsNotificationUM.YieldSupplyPromo( + onClick = { + analyticsEventHandler.send(MarketsListAnalyticsEvent.YieldModeMoreInfoClicked()) + marketsListUMStateManager.selectedSortByType = SortByTypeUM.YieldSupply + }, + onCloseClick = { onYieldModeNotificationCloseClick() }, + ) + } else { + null }, ) } @@ -266,10 +273,18 @@ internal class MarketsListModel @Inject constructor( mainMarketsListManager.reload() } + fun TrendInterval.toBatchRequestInterval(): TokenMarketListConfig.Interval { + return when (this) { + TrendInterval.H24 -> TokenMarketListConfig.Interval.H24 + TrendInterval.D7 -> TokenMarketListConfig.Interval.WEEK + TrendInterval.M1 -> TokenMarketListConfig.Interval.MONTH + } + } + 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) @@ -302,10 +317,10 @@ internal class MarketsListModel @Inject constructor( }.saveIn(updateQuotesJob) } - private fun onStakingNotificationCloseClick() { - analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoClosed) + private fun onYieldModeNotificationCloseClick() { + analyticsEventHandler.send(MarketsListAnalyticsEvent.YieldModePromoClosed()) modelScope.launch { - promoRepository.setMarketsStakingNotificationHideClicked() + promoRepository.setMarketsYieldSupplyNotificationHideClicked() } } @@ -313,7 +328,7 @@ internal class MarketsListModel @Inject constructor( val items: ImmutableList, val isInErrorState: Boolean, val isSearchNotFound: Boolean, - val stakingNotificationMaxApy: BigDecimal?, + val shouldShowYieldModePromo: Boolean, val userCountry: Either, ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsNotificationUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsNotificationUM.kt new file mode 100644 index 0000000000..5870289d78 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsNotificationUM.kt @@ -0,0 +1,22 @@ +package com.tangem.features.markets.tokenlist.impl.model + +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.markets.impl.R + +internal sealed class MarketsNotificationUM(val config: NotificationConfig) { + + data class YieldSupplyPromo( + val onClick: () -> Unit, + val onCloseClick: () -> Unit, + ) : MarketsNotificationUM( + config = NotificationConfig( + iconResId = R.drawable.img_yield_supply_in_market_notification, + title = resourceReference(R.string.markets_yield_supply_banner_title), + subtitle = TextReference.EMPTY, + onClick = onClick, + onCloseClick = onCloseClick, + ), + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index ec31aac834..2e0926eaf5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -38,7 +38,7 @@ internal class MarketsTokenItemConverter( trendType = value.getTrendType(), chartData = value.getChartData(), isUnder100kMarketCap = value.isUnderMarketCapLimit, - stakingRate = value.stakingRate?.format { percent() }?.let { + stakingRate = value.yieldRate?.format { percent() }?.let { resourceReference(R.string.markets_apy_placeholder, wrappedList(it)) }, updateTimestamp = value.updateTimestamp, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt index 03b8b24eeb..4847ff333a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt @@ -343,6 +343,7 @@ internal class MarketsListBatchFlowManager( SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers SortByTypeUM.Staking -> TokenMarketListConfig.Order.Staking + SortByTypeUM.YieldSupply -> TokenMarketListConfig.Order.YieldSupply } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt index f2c6ee64d3..33f89173a9 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.markets.impl.R +import com.tangem.features.markets.tokenlist.impl.model.MarketsNotificationUM import com.tangem.features.markets.tokenlist.impl.ui.state.* import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList @@ -16,7 +17,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update -import java.math.BigDecimal @Stable @Suppress("LongParameterList") @@ -26,8 +26,6 @@ internal class MarketsListUMStateManager( private val visibleItemsChanged: (itemsKeys: List) -> Unit, private val onRetryButtonClicked: () -> Unit, private val onTokenClick: (MarketsListItemUM) -> Unit, - private val onStakingNotificationClick: () -> Unit, - private val onStakingNotificationCloseClick: () -> Unit, private val onShowTokensUnder100kClicked: () -> Unit, ) { @@ -92,25 +90,25 @@ internal class MarketsListUMStateManager( isInErrorState: Boolean, isSearchNotFound: Boolean, uiItems: ImmutableList, - stakingNotificationMaxApy: BigDecimal?, + marketsNotificationUM: MarketsNotificationUM?, ) { - state.update { + state.update { currentState -> when { isInErrorState -> { - it.copy( + currentState.copy( list = ListUM.LoadingError(onRetryClicked = onRetryButtonClicked), ) } isSearchNotFound -> { - it.copy(list = ListUM.SearchNothingFound) + currentState.copy(list = ListUM.SearchNothingFound) } uiItems.isEmpty() -> { - it.copy(list = ListUM.Loading) + currentState.copy(list = ListUM.Loading) } else -> { - it.updateItems( + currentState.updateItems( newItems = uiItems, - stakingNotificationMaxApy = stakingNotificationMaxApy, + marketsNotificationUM = marketsNotificationUM, ) } } @@ -119,7 +117,7 @@ internal class MarketsListUMStateManager( private fun MarketsListUM.updateItems( newItems: ImmutableList, - stakingNotificationMaxApy: BigDecimal?, + marketsNotificationUM: MarketsNotificationUM?, ): MarketsListUM { val currentState = this @@ -131,7 +129,7 @@ internal class MarketsListUMStateManager( .copy( showUnder100kTokensNotificationWasHidden = currentState.showUnder100kButtonAlreadyPressed(), ), - stakingNotificationMaxApy = stakingNotificationMaxApy, + marketsNotificationUM = marketsNotificationUM, ) } @@ -225,12 +223,7 @@ internal class MarketsListUMStateManager( onOptionClicked = ::onBottomSheetOptionClicked, ), ), - stakingNotificationMaxApy = null, - onStakingNotificationClick = { - onStakingNotificationClick() - selectedSortByType = SortByTypeUM.Staking - }, - onStakingNotificationCloseClick = onStakingNotificationCloseClick, + marketsNotificationUM = null, ) private fun onBottomSheetOptionClicked(sortByTypeUM: SortByTypeUM) { 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..0930d3c1e4 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 @@ -22,7 +22,6 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -30,6 +29,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 @@ -37,20 +37,17 @@ import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.keyboardAsState -import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.* -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.percent 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.model.MarketsNotificationUM import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListSortByBottomSheet -import com.tangem.features.markets.tokenlist.impl.ui.components.StakingInMarketsPromoNotification +import com.tangem.features.markets.tokenlist.impl.ui.components.YieldSupplyInMarketsPromoNotification import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM @@ -58,7 +55,6 @@ import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetCont import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import java.math.BigDecimal private const val SHOW_MORE_KEY = "privacyPolicy" @@ -180,38 +176,38 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif ) } + val marketsNotification = state.marketsNotificationUM AnimatedVisibility( state.isInSearchMode.not() && - state.stakingNotificationMaxApy != null && - state.selectedSortBy != SortByTypeUM.Staking, + state.selectedSortBy != SortByTypeUM.YieldSupply, ) { val showMore = stringResourceSafe(R.string.common_show_more) - val description = stringResourceSafe( - R.string.markets_staking_banner_description_placeholder, - showMore, - ) - val clickableDescription = buildAnnotatedString { - append(description.substringBefore(showMore)) + when (marketsNotification) { + is MarketsNotificationUM.YieldSupplyPromo -> { + val description = stringResourceSafe( + R.string.markets_yield_supply_banner_description, + showMore, + ) - pushStringAnnotation(SHOW_MORE_KEY, "") - appendColored(showMore, TangemTheme.colors.text.accent) - pop() + val clickableDescription = annotatedReference { + append(description.substringBefore(showMore)) + + pushStringAnnotation(SHOW_MORE_KEY, "") + appendColored(showMore, TangemTheme.colors.text.accent) + pop() + } + + YieldSupplyInMarketsPromoNotification( + config = marketsNotification.config.copy( + subtitle = clickableDescription, + ), + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + ) + } + else -> { /* no-op */ + } } - - StakingInMarketsPromoNotification( - config = NotificationConfig( - iconResId = R.drawable.img_staking_in_market_notification, - title = resourceReference( - R.string.markets_staking_banner_title, - wrappedList(state.stakingNotificationMaxApy.format { percent() }), - ), - subtitle = annotatedReference(clickableDescription), - onClick = state.onStakingNotificationClick, - onCloseClick = state.onStakingNotificationCloseClick, - ), - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - ) } } } @@ -420,9 +416,10 @@ private fun Preview() { onDismissRequest = {}, content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {}, ), - stakingNotificationMaxApy = BigDecimal(0.12345), - onStakingNotificationClick = {}, - onStakingNotificationCloseClick = {}, + marketsNotificationUM = MarketsNotificationUM.YieldSupplyPromo( + onClick = {}, + onCloseClick = {}, + ), ), onHeaderSizeChange = {}, bottomSheetState = BottomSheetState.EXPANDED, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/YieldSupplyInMarketsPromoNotification.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/YieldSupplyInMarketsPromoNotification.kt new file mode 100644 index 0000000000..e2f16f2735 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/YieldSupplyInMarketsPromoNotification.kt @@ -0,0 +1,147 @@ +package com.tangem.features.markets.tokenlist.impl.ui.components + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +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.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +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 androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.notifications.CloseableIconButton +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +private val bgColor = Color(0x2684B0D7) +private val borderColor = Color(0x2684B0D7) + +@Composable +fun YieldSupplyInMarketsPromoNotification(config: NotificationConfig, modifier: Modifier = Modifier) { + var textHeightDp by remember { mutableStateOf(0.dp) } + + Box( + modifier = modifier + .fillMaxWidth() + .border( + width = 1.dp, + shape = TangemTheme.shapes.roundedCornersXMedium, + color = borderColor, + ) + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + .background(bgColor) + .clickable { config.onClick?.invoke() }, + ) { + PromoImage( + iconRes = config.iconResId, + modifier = Modifier.height(textHeightDp), + ) + PromoText( + title = config.title, + subtitle = config.subtitle, + onSizeChange = { textHeightDp = it }, + ) + CloseableIconButton( + onClick = config.onCloseClick, + modifier = Modifier.align(alignment = Alignment.TopEnd), + iconTint = TangemTheme.colors.icon.secondary, + ) + } +} + +@Composable +private fun PromoImage(@DrawableRes iconRes: Int, modifier: Modifier = Modifier) { + Box( + modifier = modifier.padding(vertical = 8.dp), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(id = iconRes), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .requiredWidth(80.dp) + .wrapContentHeight(Alignment.CenterVertically, unbounded = true), + ) + } +} + +@Composable +private fun PromoText(title: TextReference?, subtitle: TextReference, onSizeChange: (Dp) -> Unit) { + val density = LocalDensity.current + + Box( + modifier = Modifier.onSizeChanged { + with(density) { onSizeChange(it.height.toDp()) } + }, + ) { + TextsBlock( + title = title, + subtitle = subtitle, + modifier = Modifier + .wrapContentHeight() + .align(Alignment.CenterStart) + .padding(start = 80.dp, top = 12.dp, end = 12.dp, bottom = 12.dp), + ) + } +} + +@Composable +private fun TextsBlock(title: TextReference?, subtitle: TextReference, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + val titleText = title?.resolveReference() + + if (titleText != null) { + Text( + text = titleText, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.button, + ) + + SpacerH(height = TangemTheme.dimens.spacing2) + } + + Text( + text = subtitle.resolveAnnotatedReference(), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.caption2, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun RingPromoNotification_Preview() { + TangemThemePreview { + YieldSupplyInMarketsPromoNotification( + config = NotificationConfig( + title = stringReference("Activate Yield Mode"), + subtitle = stringReference("Power up your assets while supplying them with instant access. Show more"), + iconResId = R.drawable.img_yield_supply_in_market_notification, + onCloseClick = { }, + ), + ) + } +} + +// endregion \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt index da29b428d1..8404856a87 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt @@ -8,8 +8,8 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.markets.impl.R +import com.tangem.features.markets.tokenlist.impl.model.MarketsNotificationUM import kotlinx.collections.immutable.ImmutableList -import java.math.BigDecimal internal data class MarketsListUM( val list: ListUM, @@ -19,9 +19,7 @@ internal data class MarketsListUM( val selectedInterval: TrendInterval, val onIntervalClick: (TrendInterval) -> Unit, val onSortByButtonClick: () -> Unit, - val stakingNotificationMaxApy: BigDecimal?, - val onStakingNotificationClick: () -> Unit, - val onStakingNotificationCloseClick: () -> Unit, + val marketsNotificationUM: MarketsNotificationUM?, ) { val isInSearchMode get() = searchBar.isActive @@ -40,6 +38,7 @@ enum class SortByTypeUM(val text: TextReference) { TopGainers(resourceReference(R.string.markets_sort_by_top_gainers_title)), TopLosers(resourceReference(R.string.markets_sort_by_top_losers_title)), Staking(resourceReference(R.string.common_staking)), + YieldSupply(resourceReference(R.string.yield_module_earn_sheet_title)), } @Immutable 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..3dcc789481 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, @@ -111,6 +111,7 @@ internal class AllOffersStateFactory( -> true is OnrampError.AmountError, is OnrampError.RedirectError, + is OnrampError.AlreadyHandledTransaction, -> false } } 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/OnrampStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt index b0a9d7773a..8faf435657 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt @@ -63,6 +63,7 @@ internal class OnrampStateFactory( is OnrampError.AmountError.TooSmallError, OnrampError.RedirectError.VerificationFailed, OnrampError.RedirectError.WrongRequestId, + OnrampError.AlreadyHandledTransaction, -> currentStateProvider() // ignore error state } } 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/entity/factory/OnrampV2StateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt index ad9a4c4577..06ec867696 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt @@ -86,6 +86,7 @@ internal class OnrampV2StateFactory( is OnrampError.AmountError.TooSmallError, OnrampError.RedirectError.VerificationFailed, OnrampError.RedirectError.WrongRequestId, + OnrampError.AlreadyHandledTransaction, -> currentStateProvider() // ignore error state } } 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..15b4eed93a 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) } @@ -177,10 +177,12 @@ internal class OnrampSuccessComponentModel @Inject constructor( private fun showErrorAlert(error: OnrampError) { val errorCode = (error as? OnrampError.DataError)?.code val message = DialogMessage( - message = if (errorCode.isNullOrBlank()) { - resourceReference(R.string.common_unknown_error) - } else { - resourceReference(R.string.express_error_code, wrappedList(errorCode)) + message = when { + !errorCode.isNullOrBlank() -> resourceReference(R.string.express_error_code, wrappedList(errorCode)) + error is OnrampError.AlreadyHandledTransaction -> resourceReference( + R.string.onramp_error_transaction_already_processed, + ) + else -> resourceReference(R.string.common_unknown_error) }, firstActionBuilder = { okAction(router::pop) 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/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampErrorAnalyticsSender.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampErrorAnalyticsSender.kt index e9ec5c76c5..f39254139d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampErrorAnalyticsSender.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampErrorAnalyticsSender.kt @@ -31,5 +31,6 @@ internal fun AnalyticsEventHandler.sendOnrampErrorEvent( OnrampError.RedirectError.VerificationFailed, OnrampError.RedirectError.WrongRequestId, OnrampError.PairsNotFound, + OnrampError.AlreadyHandledTransaction, -> { /* no-op */ } } \ No newline at end of file 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 70e39f6f9b..a24e274c2e 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 @@ -191,9 +191,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() } @@ -278,10 +278,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 } @@ -467,7 +467,7 @@ internal class StakingModel @Inject constructor( } override fun onInitialInfoBannerClick() { - analyticsEventHandler.send(StakingAnalyticsEvent.WhatIsStaking) + analyticsEventHandler.send(StakingAnalyticsEvent.WhatIsStaking()) innerRouter.openUrl(WHAT_IS_STAKING_ARTICLE_URL) } @@ -513,7 +513,7 @@ internal class StakingModel @Inject constructor( } override fun onMaxValueClick() { - analyticsEventHandler.send(StakingAnalyticsEvent.ButtonMax) + analyticsEventHandler.send(StakingAnalyticsEvent.ButtonMax()) stateController.update( AmountMaxValueStateTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, @@ -555,7 +555,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 @@ -890,7 +890,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 @@ -904,7 +904,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) @@ -1128,7 +1128,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 5968fa4437..45351b95ed 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 1953adbd19..15b59a6ef4 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 @@ -100,8 +100,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, @@ -143,9 +143,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/data/detekt-baseline-debug.xml b/features/swap/data/detekt-baseline-debug.xml deleted file mode 100644 index 640fd2fbc1..0000000000 --- a/features/swap/data/detekt-baseline-debug.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - BooleanPropertyNaming:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository$val fromCurrency = it.fromCryptoCurrencyId == cryptoCurrencyId.value - BooleanPropertyNaming:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository$val toCurrency = it.toCryptoCurrencyId == cryptoCurrencyId.value - MultilineLambdaItParameter:DefaultSwapRepository.kt$DefaultSwapRepository${ Timber.e("getExchangeStatus error: $it") raise(UnknownError(it.message)) } - MultilineLambdaItParameter:DefaultSwapRepository.kt$DefaultSwapRepository${ val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, it) val isAvailableForSwap = rampStateManager.checkAssetRequirements(requirements) isAvailableForSwap } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ converter.convertBack( value = it, userWallet = userWallet, accountList = accountList, txStatuses = txStatuses, ) } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ converter.convertBack( value = it, userWallet = userWallet, accountList = accountList, txStatuses = txStatuses, onFilter = { it.swapTxTypeDTO == SwapTxTypeDTO.Swap }, ) } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ it.checkId( checkUserWalletId = userWalletId, fromCurrencyId = fromCryptoCurrency.id, toCurrencyId = toCryptoCurrency.id, ) } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ storeTransactionState( txId = transaction.txId, status = it, accountWithCurrency = null, ) } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ val isUserWallet = it.userWalletId == userWallet.walletId.stringValue val fromCurrency = it.fromCryptoCurrencyId == cryptoCurrencyId.value isUserWallet && fromCurrency } - MultilineLambdaItParameter:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ val isUserWallet = it.userWalletId == userWallet.walletId.stringValue val toCurrency = it.toCryptoCurrencyId == cryptoCurrencyId.value isUserWallet && toCurrency } - MultilineLambdaItParameter:ExpressDataConverter.kt$ExpressDataConverter${ if (it == "0") { BigDecimal.ZERO } else { requireNotNull(it.toBigDecimalOrNull()) { "wrong amount format, use only digits" } } } - NoNameShadowing:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ it.swapTxTypeDTO == SwapTxTypeDTO.Swap } - NoNameShadowing:DefaultSwapTransactionRepository.kt$DefaultSwapTransactionRepository${ it.txId == txId } - UseOrEmpty:DefaultSwapRepository.kt$DefaultSwapRepository$ex.errorBody ?: "" - UseOrEmpty:DefaultSwapRepository.kt$DefaultSwapRepository$exception.errorBody ?: "" - - diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index 2f34e89a84..2b464a5d59 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -118,7 +118,7 @@ internal class DefaultSwapRepository( ) } catch (exception: Exception) { if (exception is ApiResponseError.HttpException) { - throw ExpressException(errorsDataConverter.convert(exception.errorBody ?: "")) + throw ExpressException(errorsDataConverter.convert(exception.errorBody.orEmpty())) } else { throw exception } @@ -138,12 +138,12 @@ internal class DefaultSwapRepository( network = initialCurrency.network, ) val currenciesList = currencyList - .filter { - val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, it) + .filter { currency -> + val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, currency) val isAvailableForSwap = rampStateManager.checkAssetRequirements(requirements) isAvailableForSwap } - .map { leastTokenInfoConverter.convert(it) } + .map { currency -> leastTokenInfoConverter.convert(currency) } val pairsDeferred = async { getPairsInternal( @@ -174,7 +174,7 @@ internal class DefaultSwapRepository( ) } catch (exception: Exception) { if (exception is ApiResponseError.HttpException) { - throw ExpressException(errorsDataConverter.convert(exception.errorBody ?: "")) + throw ExpressException(errorsDataConverter.convert(exception.errorBody.orEmpty())) } else { throw exception } @@ -221,9 +221,9 @@ internal class DefaultSwapRepository( .getOrThrow(), ) }, - catch = { - Timber.e("getExchangeStatus error: $it") - raise(UnknownError(it.message)) + catch = { exception -> + Timber.e("getExchangeStatus error: $exception") + raise(UnknownError(exception.message)) }, ) } @@ -420,7 +420,7 @@ internal class DefaultSwapRepository( private fun getDataError(ex: Exception): ExpressDataError { return if (ex is ApiResponseError.HttpException) { - errorsDataConverter.convert(ex.errorBody ?: "") + errorsDataConverter.convert(ex.errorBody.orEmpty()) } else { ExpressDataError.UnknownError } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt index ab4f58a5df..fb7b9d0ed1 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt @@ -51,10 +51,10 @@ internal class DefaultSwapTransactionRepository( toAccount: Account.CryptoPortfolio?, transaction: SavedSwapTransactionModel, ) { - transaction.status?.let { + transaction.status?.let { status -> storeTransactionState( txId = transaction.txId, - status = it, + status = status, accountWithCurrency = null, ) } @@ -63,8 +63,8 @@ internal class DefaultSwapTransactionRepository( key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, ) val tokenTransactions = savedTransactions - ?.firstOrNull { - it.checkId( + ?.firstOrNull { savedTx -> + savedTx.checkId( checkUserWalletId = userWalletId, fromCurrencyId = fromCryptoCurrency.id, toCurrencyId = toCryptoCurrency.id, @@ -73,7 +73,7 @@ internal class DefaultSwapTransactionRepository( ?.transactions ?.addOrReplace( item = transaction, - predicate = { it.txId == transaction.txId }, + predicate = { tx -> tx.txId == transaction.txId }, ) ?: listOf(transaction) mutablePreferences.setObject( @@ -117,31 +117,31 @@ internal class DefaultSwapTransactionRepository( }, ) { savedTransactions, txStatuses, accountList -> - val currencyToTxs = savedTransactions?.filter { - val isUserWallet = it.userWalletId == userWallet.walletId.stringValue - val toCurrency = it.toCryptoCurrencyId == cryptoCurrencyId.value - isUserWallet && toCurrency + val currencyToTxs = savedTransactions?.filter { savedTx -> + val isUserWallet = savedTx.userWalletId == userWallet.walletId.stringValue + val isToCurrency = savedTx.toCryptoCurrencyId == cryptoCurrencyId.value + isUserWallet && isToCurrency } - val currencyFromTxs = savedTransactions?.filter { - val isUserWallet = it.userWalletId == userWallet.walletId.stringValue - val fromCurrency = it.fromCryptoCurrencyId == cryptoCurrencyId.value - isUserWallet && fromCurrency + val currencyFromTxs = savedTransactions?.filter { savedTx -> + val isUserWallet = savedTx.userWalletId == userWallet.walletId.stringValue + val isFromCurrency = savedTx.fromCryptoCurrencyId == cryptoCurrencyId.value + isUserWallet && isFromCurrency } - val toTxs = currencyToTxs?.mapNotNull { + val toTxs = currencyToTxs?.mapNotNull { savedTx -> converter.convertBack( - value = it, + value = savedTx, userWallet = userWallet, accountList = accountList, txStatuses = txStatuses, - onFilter = { it.swapTxTypeDTO == SwapTxTypeDTO.Swap }, + onFilter = { tx -> tx.swapTxTypeDTO == SwapTxTypeDTO.Swap }, ) }.orEmpty() - val fromTxs = currencyFromTxs?.mapNotNull { + val fromTxs = currencyFromTxs?.mapNotNull { savedTx -> converter.convertBack( - value = it, + value = savedTx, userWallet = userWallet, accountList = accountList, txStatuses = txStatuses, @@ -161,9 +161,9 @@ internal class DefaultSwapTransactionRepository( ) val tokenTransactions = savedList ?.asSequence() - ?.map { - it.copy(transactions = it.transactions.filterNot { it.txId == txId }) - }?.filterNot { it.transactions.isEmpty() } + ?.map { savedTx -> + savedTx.copy(transactions = savedTx.transactions.filterNot { tx -> tx.txId == txId }) + }?.filterNot { savedTx -> savedTx.transactions.isEmpty() } ?.toList() if (tokenTransactions.isNullOrEmpty()) { @@ -261,8 +261,8 @@ internal class DefaultSwapTransactionRepository( toAccount = toAccount, tokenTransactions = transactions, ), - predicate = { - it.checkId( + predicate = { savedTx -> + savedTx.checkId( checkUserWalletId = userWalletId, fromCurrencyId = fromCryptoCurrency.id, toCurrencyId = toCryptoCurrency.id, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt index 34c663840b..8d605d8dfd 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt @@ -25,11 +25,11 @@ internal class ExpressDataConverter : Converter + if (feeValue == "0") { BigDecimal.ZERO } else { - requireNotNull(it.toBigDecimalOrNull()) { "wrong amount format, use only digits" } + requireNotNull(feeValue.toBigDecimalOrNull()) { "wrong amount format, use only digits" } } } ExpressTransactionModel.DEX( diff --git a/features/swap/domain/detekt-baseline-debug.xml b/features/swap/domain/detekt-baseline-debug.xml deleted file mode 100644 index e1d3527ea0..0000000000 --- a/features/swap/domain/detekt-baseline-debug.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - BooleanPropertyNaming:SwapInteractorImpl.kt$SwapInteractorImpl$val allQuotesFound = cachedQuotes?.all { it.value !is QuoteStatus.Empty } == true - BooleanPropertyNaming:SwapInteractorImpl.kt$SwapInteractorImpl$val currencyFilter = it.currency.network.backendId != currency.network.backendId || it.currency.getContractAddress() != currency.getContractAddress() - BooleanPropertyNaming:SwapInteractorImpl.kt$SwapInteractorImpl$val notCustomTokenFilter = !it.currency.isCustom - BooleanPropertyNaming:SwapInteractorImpl.kt$SwapInteractorImpl$val statusFilter = it.value is CryptoCurrencyStatus.Loaded || it.value is CryptoCurrencyStatus.NoAccount - CanBeNonNullable:SwapInteractorImpl.kt$SwapInteractorImpl$spenderAddress: String? - MaxChainedCallsOnSameLine:SwapInteractorImpl.kt$SwapInteractorImpl$currencyToGet.value.networkAddress?.defaultAddress?.value.orEmpty() - MaxChainedCallsOnSameLine:SwapInteractorImpl.kt$SwapInteractorImpl$currencyToSend.value.networkAddress?.defaultAddress?.value.orEmpty() - MaxChainedCallsOnSameLine:SwapInteractorImpl.kt$SwapInteractorImpl$currencyToSendStatus.value.networkAddress?.defaultAddress?.value.orEmpty() - MaxChainedCallsOnSameLine:SwapInteractorImpl.kt$SwapInteractorImpl$fromToken.value.networkAddress?.defaultAddress?.value.orEmpty() - MaxChainedCallsOnSameLine:SwapInteractorImpl.kt$SwapInteractorImpl$toToken.value.networkAddress?.defaultAddress?.value.orEmpty() - MultilineLambdaItParameter:SwapInteractorImpl.kt$SwapInteractorImpl${ Timber.e(it, "Failed to create approveTransaction") return SwapTransactionState.Error.UnknownError } - MultilineLambdaItParameter:SwapInteractorImpl.kt$SwapInteractorImpl${ Timber.e(it, "Failed to create swap CEX tx data") return SwapTransactionState.Error.UnknownError } - MultilineLambdaItParameter:SwapInteractorImpl.kt$SwapInteractorImpl${ Timber.e(it, "Failed to create swap dex tx data") return SwapTransactionState.Error.UnknownError } - MultilineLambdaItParameter:SwapInteractorImpl.kt$SwapInteractorImpl${ it.contractAddress.equals(feePaidCurrency.contractAddress, ignoreCase = true) && it.network.derivationPath == fromTokenStatus.currency.network.derivationPath } - MultilineLambdaItParameter:SwapInteractorImpl.kt$SwapInteractorImpl${ tokenInfoForFilter(it).contractAddress == currency.getContractAddress() && tokenInfoForFilter(it).network == currency.network.backendId } - MultilineLambdaItParameter:SwapInteractorImpl.kt$SwapInteractorImpl${ val currencyFilter = it.currency.network.backendId != currency.network.backendId || it.currency.getContractAddress() != currency.getContractAddress() val statusFilter = it.value is CryptoCurrencyStatus.Loaded || it.value is CryptoCurrencyStatus.NoAccount val notCustomTokenFilter = !it.currency.isCustom statusFilter && currencyFilter && notCustomTokenFilter } - MultilineLambdaItParameter:SwapInteractorImpl.kt$SwapInteractorImpl${ val listTokenInfo = tokenInfoForAvailable(it) if (cryptoCurrencyStatuses.currency.network.backendId == listTokenInfo.network && cryptoCurrencyStatuses.currency.getContractAddress() == listTokenInfo.contractAddress && isAvailableForSwap ) { it.providers } else { null } } - NamedArguments:SwapInteractorImpl.kt$SwapInteractorImpl$getCoinBalanceAfterTransaction(fromTokenStatus, amount, includeFeeInAmount, fee) - NamedArguments:SwapInteractorImpl.kt$SwapInteractorImpl$isAllowedToSpend(networkId, fromToken.currency, amount, it) - NamedArguments:SwapInteractorImpl.kt$SwapInteractorImpl$tryGetFromCache(userWallet, initialCryptoCurrency, state, isReverseFromTo) - NamedArguments:SwapInteractorImpl.kt$SwapInteractorImpl$tryGetFromCacheV2(userWallet, initialCryptoCurrency, state, isReverseFromTo) - NoNameShadowing:SwapInteractorImpl.kt$SwapInteractorImpl$account - NoNameShadowing:SwapInteractorImpl.kt$SwapInteractorImpl${ it.isAvailable } - NullableToStringCall:SwapInteractorImpl.kt$SwapInteractorImpl$${e.message} - SuspendFunSwallowedCancellation:SwapInteractorImpl.kt$SwapInteractorImpl$runCatching - - diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 0531735446..9121e9d618 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -45,6 +45,7 @@ import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.* import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount @@ -124,12 +125,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( .getOrElse { emptyList() } val walletCurrencyStatusesExceptInitial = walletCurrencyStatuses - .filter { - val currencyFilter = it.currency.network.backendId != currency.network.backendId || - it.currency.getContractAddress() != currency.getContractAddress() - val statusFilter = it.value is CryptoCurrencyStatus.Loaded || it.value is CryptoCurrencyStatus.NoAccount - val notCustomTokenFilter = !it.currency.isCustom - statusFilter && currencyFilter && notCustomTokenFilter + .filter { status -> + val isDifferentCurrency = status.currency.network.backendId != currency.network.backendId || + status.currency.getContractAddress() != currency.getContractAddress() + val hasValidStatus = + status.value is CryptoCurrencyStatus.Loaded || status.value is CryptoCurrencyStatus.NoAccount + val isNotCustomToken = !status.currency.isCustom + hasValidStatus && isDifferentCurrency && isNotCustomToken } if (walletCurrencyStatusesExceptInitial.isEmpty()) { @@ -171,15 +173,15 @@ internal class SwapInteractorImpl @AssistedInject constructor( val walletAccountCurrencyStatusesExceptInitial = walletAccountCurrencyStatuses .mapNotNull { accountStatus -> - val filteredCurrencies = accountStatus.flattenCurrencies().filter { - val currencyFilter = it.currency.network.backendId != currency.network.backendId || - it.currency.getContractAddress() != currency.getContractAddress() + val filteredCurrencies = accountStatus.flattenCurrencies().filter { status -> + val isDifferentCurrency = status.currency.network.backendId != currency.network.backendId || + status.currency.getContractAddress() != currency.getContractAddress() - val statusFilter = - it.value is CryptoCurrencyStatus.Loaded || it.value is CryptoCurrencyStatus.NoAccount - val notCustomTokenFilter = !it.currency.isCustom + val hasValidStatus = + status.value is CryptoCurrencyStatus.Loaded || status.value is CryptoCurrencyStatus.NoAccount + val isNotCustomToken = !status.currency.isCustom - statusFilter && currencyFilter && notCustomTokenFilter + hasValidStatus && isDifferentCurrency && isNotCustomToken } if (filteredCurrencies.isNotEmpty()) { @@ -230,9 +232,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( tokenInfoForFilter: (SwapPairLeast) -> LeastTokenInfo, tokenInfoForAvailable: (SwapPairLeast) -> LeastTokenInfo, ): CurrenciesGroup { - val filteredPairs = leastPairs.filter { - tokenInfoForFilter(it).contractAddress == currency.getContractAddress() && - tokenInfoForFilter(it).network == currency.network.backendId + val filteredPairs = leastPairs.filter { pair -> + tokenInfoForFilter(pair).contractAddress == currency.getContractAddress() && + tokenInfoForFilter(pair).network == currency.network.backendId } val availableCryptoCurrencies = cryptoCurrenciesList.mapNotNull { cryptoCurrencyStatus -> @@ -263,16 +265,16 @@ internal class SwapInteractorImpl @AssistedInject constructor( tokenInfoForFilter: (SwapPairLeast) -> LeastTokenInfo, tokenInfoForAvailable: (SwapPairLeast) -> LeastTokenInfo, ): CurrenciesGroup { - val filteredPairs = leastPairs.filter { - tokenInfoForFilter(it).contractAddress == currency.getContractAddress() && - tokenInfoForFilter(it).network == currency.network.backendId + val filteredPairs = leastPairs.filter { pair -> + tokenInfoForFilter(pair).contractAddress == currency.getContractAddress() && + tokenInfoForFilter(pair).network == currency.network.backendId } - val accountCurrencyList = cryptoCurrenciesList.mapNotNull { (account, currencyStatusList) -> - val account = account as? Account.CryptoPortfolio ?: return@mapNotNull null + val accountCurrencyList = cryptoCurrenciesList.mapNotNull { (accountEntry, currencyStatusList) -> + val cryptoPortfolio = accountEntry as? Account.CryptoPortfolio ?: return@mapNotNull null AccountSwapAvailability( - account = account, + account = cryptoPortfolio, currencyList = currencyStatusList.map { currencyStatus -> val providers = findProvidersForPair( cryptoCurrencyStatuses = currencyStatus, @@ -282,7 +284,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val isUnavailable = providers.isNullOrEmpty() AccountSwapCurrency( isAvailable = !isUnavailable, - account = account, + account = cryptoPortfolio, cryptoCurrencyStatus = currencyStatus, providers = providers.orEmpty(), ) @@ -306,13 +308,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( val requirements = getAssetRequirementsUseCase.invoke(userWalletId, cryptoCurrencyStatuses.currency).getOrNull() val isAvailableForSwap = rampStateManager.checkAssetRequirements(requirements) - return swapPairsLeastList.firstNotNullOfOrNull { - val listTokenInfo = tokenInfoForAvailable(it) + return swapPairsLeastList.firstNotNullOfOrNull { pair -> + val listTokenInfo = tokenInfoForAvailable(pair) if (cryptoCurrencyStatuses.currency.network.backendId == listTokenInfo.network && cryptoCurrencyStatuses.currency.getContractAddress() == listTokenInfo.contractAddress && isAvailableForSwap ) { - it.providers + pair.providers } else { null } @@ -353,8 +355,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( amount = amount?.value, contractAddress = permissionOptions.forTokenContractAddress, spenderAddress = permissionOptions.spenderAddress, - ).getOrElse { - Timber.e(it, "Failed to create approveTransaction") + ).getOrElse { error -> + Timber.e(error, "Failed to create approveTransaction") return SwapTransactionState.Error.UnknownError } @@ -452,6 +454,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( }.toMap() } + @Suppress("LongMethod") private suspend fun manageDex( networkId: String, fromToken: CryptoCurrencyStatus, @@ -489,8 +492,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( val fromTokenAddress = getTokenAddress(fromToken.currency) val isAllowedToSpend = maybeQuotes.fold( ifRight = { quotes -> - quotes.allowanceContract?.let { - isAllowedToSpend(networkId, fromToken.currency, amount, it) + quotes.allowanceContract?.let { allowanceContract -> + isAllowedToSpend( + networkId = networkId, + fromToken = fromToken.currency, + amount = amount, + spenderAddress = allowanceContract, + ) } != false }, ifLeft = { false }, @@ -628,7 +636,12 @@ internal class SwapInteractorImpl @AssistedInject constructor( is TxFeeState.MultipleFeeState -> feeState.getFeeByType(selectedFee).feeValue is TxFeeState.SingleFeeState -> feeState.fee.feeValue } - val balanceAfterTransaction = getCoinBalanceAfterTransaction(fromTokenStatus, amount, includeFeeInAmount, fee) + val balanceAfterTransaction = getCoinBalanceAfterTransaction( + fromTokenStatus = fromTokenStatus, + amount = amount, + includeFeeInAmount = includeFeeInAmount, + fee = fee, + ) val amountToRequest = if (includeFeeInAmount is IncludeFeeInAmount.Included) { includeFeeInAmount.amountSubtractFee } else { @@ -868,8 +881,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( userWalletId = userWalletId, network = currencyToSendStatus.currency.network, txExtras = createDexTxExtras(dataToSign, currencyToSendStatus.currency.network, txFee.fee.getGasLimit()), - ).getOrElse { - Timber.e(it, "Failed to create swap dex tx data") + ).getOrElse { error -> + Timber.e(error, "Failed to create swap dex tx data") return SwapTransactionState.Error.UnknownError } @@ -941,11 +954,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) return result.fold( ifRight = { txHash -> + val networkAddress = currencyToSendStatus.value.networkAddress + val fromAddress = networkAddress?.defaultAddress?.value.orEmpty() repository.exchangeSent( userWallet = userWallet, txId = swapData.transaction.txId, fromNetwork = currencyToSendStatus.currency.network.backendId, - fromAddress = currencyToSendStatus.value.networkAddress?.defaultAddress?.value.orEmpty(), + fromAddress = fromAddress, payInAddress = payInAddress, txHash = txHash, payInExtraId = swapData.transaction.txExtraId, @@ -1003,12 +1018,16 @@ internal class SwapInteractorImpl @AssistedInject constructor( expressOperationType: ExpressOperationType, isTangemPayWithdrawal: Boolean, ): SwapTransactionState { + val fromNetworkAddress = currencyToSend.value.networkAddress + val fromAddress = fromNetworkAddress?.defaultAddress?.value.orEmpty() + val toNetworkAddress = currencyToGet.value.networkAddress + val toAddress = toNetworkAddress?.defaultAddress?.value.orEmpty() val exchangeData = repository.getExchangeData( userWallet = userWallet, fromContractAddress = currencyToSend.currency.getContractAddress(), fromNetwork = currencyToSend.currency.network.backendId, toContractAddress = currencyToGet.currency.getContractAddress(), - fromAddress = currencyToSend.value.networkAddress?.defaultAddress?.value.orEmpty(), + fromAddress = fromAddress, toNetwork = currencyToGet.currency.network.backendId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, @@ -1016,10 +1035,10 @@ internal class SwapInteractorImpl @AssistedInject constructor( providerId = swapProvider.providerId, rateType = RateType.FLOAT, expressOperationType = expressOperationType, - toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value.orEmpty(), + toAddress = toAddress, refundAddress = currencyToSend.value.networkAddress?.defaultAddress?.value, refundExtraId = null, // currently always null, - ).getOrElse { return SwapTransactionState.Error.ExpressError(it) } + ).getOrElse { error -> return SwapTransactionState.Error.ExpressError(error) } val exchangeDataCex = exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.Error.UnknownError @@ -1066,8 +1085,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( destination = exchangeDataCex.txTo, userWalletId = userWalletId, network = currencyToSend.currency.network, - ).getOrElse { - Timber.e(it, "Failed to create swap CEX tx data") + ).getOrElse { error -> + Timber.e(error, "Failed to create swap CEX tx data") return SwapTransactionState.Error.UnknownError } @@ -1082,14 +1101,16 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) val derivationPath = currencyToSend.currency.network.derivationPath.value + val cexNetworkAddress = currencyToSend.value.networkAddress + val cexFromAddress = cexNetworkAddress?.defaultAddress?.value.orEmpty() return result.fold( - ifLeft = { SwapTransactionState.Error.TransactionError(it) }, + ifLeft = { error -> SwapTransactionState.Error.TransactionError(error) }, ifRight = { txHash -> repository.exchangeSent( userWallet = userWallet, txId = exchangeDataCex.txId, fromNetwork = currencyToSend.currency.network.backendId, - fromAddress = currencyToSend.value.networkAddress?.defaultAddress?.value.orEmpty(), + fromAddress = cexFromAddress, payInAddress = getPayoutAddress(txData), txHash = txHash, payInExtraId = exchangeDataCex.txExtraId, @@ -1185,8 +1206,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( isReverseFromTo: Boolean, ): CryptoCurrencyStatus? { val group = state.getGroupWithReverse(isReverseFromTo) - return initialToCurrencyResolver.tryGetFromCache(userWallet, initialCryptoCurrency, state, isReverseFromTo) - ?: initialToCurrencyResolver.tryGetWithMaxAmount(state, isReverseFromTo) + return initialToCurrencyResolver.tryGetFromCache( + userWallet = userWallet, + initialCryptoCurrency = initialCryptoCurrency, + state = state, + isReverseFromTo = isReverseFromTo, + ) + ?: initialToCurrencyResolver.tryGetWithMaxAmount(state = state, isReverseFromTo = isReverseFromTo) ?: group.available.firstOrNull()?.currencyStatus } @@ -1196,9 +1222,18 @@ internal class SwapInteractorImpl @AssistedInject constructor( isReverseFromTo: Boolean, ): AccountSwapCurrency? { val group = state.getGroupWithReverse(isReverseFromTo) - return initialToCurrencyResolver.tryGetFromCacheV2(userWallet, initialCryptoCurrency, state, isReverseFromTo) - ?: initialToCurrencyResolver.tryGetWithMaxAmountV2(state, isReverseFromTo) - ?: group.accountCurrencyList.firstNotNullOfOrNull { it.currencyList.firstOrNull { it.isAvailable } } + return initialToCurrencyResolver.tryGetFromCacheV2( + userWallet = userWallet, + initialCryptoCurrency = initialCryptoCurrency, + state = state, + isReverseFromTo = isReverseFromTo, + ) + ?: initialToCurrencyResolver.tryGetWithMaxAmountV2(state = state, isReverseFromTo = isReverseFromTo) + ?: group.accountCurrencyList.firstNotNullOfOrNull { accountSwapAvailability -> + accountSwapAvailability.currencyList.firstOrNull { accountSwapCurrency -> + accountSwapCurrency.isAvailable + } + } } override fun getNativeToken(networkId: String): CryptoCurrency { @@ -1370,7 +1405,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( swapAmount = amount, quotesLoadedState = swapState, isAllowedToSpend = isAllowedToSpend, - spenderAddress = quoteModel.allowanceContract, + spenderAddress = requireNotNull(quoteModel.allowanceContract) { + "allowanceContract is required for DEX" + }, ) if (state !is SwapState.QuotesLoadedState) return state state.copy( @@ -1551,19 +1588,23 @@ internal class SwapInteractorImpl @AssistedInject constructor( selectedFee: FeeType, expressOperationType: ExpressOperationType, ): SwapState { + val fromNetworkAddress = fromToken.value.networkAddress + val dexFromAddress = fromNetworkAddress?.defaultAddress?.value.orEmpty() + val toNetworkAddress = toToken.value.networkAddress + val dexToAddress = toNetworkAddress?.defaultAddress?.value.orEmpty() return repository.getExchangeData( userWallet = userWallet, fromContractAddress = fromToken.currency.getContractAddress(), fromNetwork = fromToken.currency.network.backendId, toContractAddress = toToken.currency.getContractAddress(), - fromAddress = fromToken.value.networkAddress?.defaultAddress?.value.orEmpty(), + fromAddress = dexFromAddress, toNetwork = toToken.currency.network.backendId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, toDecimals = toToken.currency.decimals, providerId = provider.providerId, rateType = RateType.FLOAT, - toAddress = toToken.value.networkAddress?.defaultAddress?.value.orEmpty(), + toAddress = dexToAddress, refundAddress = fromToken.value.networkAddress?.defaultAddress?.value, expressOperationType = expressOperationType, ).fold( @@ -1816,7 +1857,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( fromAccount: Account.CryptoPortfolio?, swapAmount: SwapAmount, quotesLoadedState: SwapState.QuotesLoadedState, - spenderAddress: String?, + spenderAddress: String, isAllowedToSpend: Boolean, ): SwapState { val fromToken = fromTokenStatus.currency @@ -1840,7 +1881,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val derivationPath = fromToken.network.derivationPath.value // setting up amount for approve with given amount for swap [SwapApproveType.Limited] val callData = SmartContractCallDataProviderFactory.getApprovalCallData( - spenderAddress = requireNotNull(spenderAddress) { "Spender address is null" }, + spenderAddress = spenderAddress, amount = swapAmount.value.convertToSdkAmount(fromTokenStatus), blockchain = fromToken.network.toBlockchain(), ) @@ -2215,9 +2256,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( val token = tokens .filterIsInstance() - .find { - it.contractAddress.equals(feePaidCurrency.contractAddress, ignoreCase = true) && - it.network.derivationPath == fromTokenStatus.currency.network.derivationPath + .find { cryptoToken -> + cryptoToken.contractAddress.equals(feePaidCurrency.contractAddress, ignoreCase = true) && + cryptoToken.network.derivationPath == fromTokenStatus.currency.network.derivationPath } SwapFeeState.NotEnough( @@ -2271,18 +2312,18 @@ internal class SwapInteractorImpl @AssistedInject constructor( } private suspend fun Set.getQuotesOrEmpty(): Set { - return runCatching { - val cachedQuotes = quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = this) + return runSuspendCatching { + val cachedQuotes = quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = this@getQuotesOrEmpty) - val allQuotesFound = cachedQuotes?.all { it.value !is QuoteStatus.Empty } == true + val areAllQuotesFound = cachedQuotes?.all { quote -> quote.value !is QuoteStatus.Empty } == true - if (allQuotesFound) return@runCatching cachedQuotes + if (areAllQuotesFound) return@runSuspendCatching cachedQuotes.orEmpty() val currenciesIds = if (cachedQuotes.isNullOrEmpty()) { - this + this@getQuotesOrEmpty } else { - cachedQuotes.mapNotNullTo(hashSetOf()) { - if (it.value is QuoteStatus.Empty) it.rawCurrencyId else null + cachedQuotes.mapNotNullTo(hashSetOf()) { quote -> + if (quote.value is QuoteStatus.Empty) quote.rawCurrencyId else null } } @@ -2290,10 +2331,11 @@ internal class SwapInteractorImpl @AssistedInject constructor( params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null), ) - quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = this) + quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = this@getQuotesOrEmpty).orEmpty() + }.getOrElse { e -> + Timber.e(e, "Failed to get quotes: ${e.message.orEmpty()}") + emptySet() } - .getOrNull() - .orEmpty() } private fun isSolana(networkId: String): Boolean { @@ -2305,7 +2347,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( return try { SolanaTransactionHelper.removeSignaturesPlaceholders(hash) } catch (e: Exception) { - Timber.e("Failed to format the hash: ${e.message}") + Timber.e("Failed to format the hash: ${e.message.orEmpty()}") hash } } diff --git a/features/swap/impl/detekt-baseline-debug.xml b/features/swap/impl/detekt-baseline-debug.xml deleted file mode 100644 index 44b4ff0d46..0000000000 --- a/features/swap/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - - BooleanPropertyNaming:ChooseFeeBottomSheet.kt$val showDivider = content.feeItems.lastIndex != index - BooleanPropertyNaming:SwapEvents.kt$SwapEvents.ChooseTokenScreenOpened$val availableTokens: Boolean - BooleanPropertyNaming:SwapEvents.kt$SwapEvents.ChooseTokenScreenResult$val tokenChosen: Boolean - BooleanPropertyNaming:SwapNotificationsFactory.kt$SwapNotificationsFactory$val needShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough && quoteModel.permissionState !is PermissionDataState.PermissionLoading && feeEnoughState.feeCurrency != fromToken - BooleanPropertyNaming:SwapSelectTokenStateHolder.kt$SwapSelectTokenStateHolder$val afterSearch: Boolean - BooleanPropertyNaming:SwapSelectTokenStateHolder.kt$TokenToSelectState.TokenToSelect$val available: Boolean = true - BooleanPropertyNaming:SwapStateHolder.kt$SwapButton$val enabled: Boolean - BooleanPropertyNaming:SwapStateHolder.kt$TransactionCardType.ReadOnly$val showWarning: Boolean = false - BooleanPropertyNaming:SwapSuccessStateHolder.kt$SwapSuccessStateHolder$val showStatusButton: Boolean - CastNullableToNonNullableType:SwapModel.kt$SwapModel$as - MaxChainedCallsOnSameLine:SwapModel.kt$SwapModel$it.value.toTokenInfo.cryptoCurrencyStatus.currency.decimals - MaxChainedCallsOnSameLine:SwapNotificationsFactory.kt$SwapNotificationsFactory$quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency.network.currencySymbol - MultilineLambdaItParameter:AccountTokenItemConverter.kt$AccountTokenItemConverter${ TokenItemState.TitleState.Content( text = stringReference(value = it.currency.name), isAvailable = false, ) } - MultilineLambdaItParameter:AccountTokenItemConverter.kt$AccountTokenItemConverter${ createSubtitleState( status = it, isAvailable = false, text = unavailableErrorText, ) } - MultilineLambdaItParameter:AccountTokenItemConverter.kt$AccountTokenItemConverter${ createSubtitleState( status = it, isAvailable = true, text = stringReference(value = it.currency.symbol), ) } - MultilineLambdaItParameter:ProviderItem.kt${ Text( text = if (it > 0) "+$it%" else "$it%", style = TangemTheme.typography.body2, color = textColor, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), overflow = TextOverflow.Ellipsis, maxLines = 1, ) } - MultilineLambdaItParameter:ProviderItem.kt${ Text( text = it, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.primary1, ) } - MultilineLambdaItParameter:ProviderItem.kt${ Text( text = it, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) } - MultilineLambdaItParameter:ProviderItem.kt${ Text( text = it, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), ) } - MultilineLambdaItParameter:ProviderItem.kt${ Text( text = it.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(top = TangemTheme.dimens.spacing6), ) } - MultilineLambdaItParameter:ProviderItem.kt${ Text( text = it.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, overflow = TextOverflow.Ellipsis, maxLines = 1, ) } - MultilineLambdaItParameter:StateBuilder.kt$StateBuilder${ LegalState( title = resourceReference(R.string.common_privacy_policy), link = it, onClick = actions.onLinkClick, ) } - MultilineLambdaItParameter:StateBuilder.kt$StateBuilder${ LegalState( title = resourceReference(R.string.common_terms_of_use), link = it, onClick = actions.onLinkClick, ) } - MultilineLambdaItParameter:StateBuilder.kt$StateBuilder${ it is SwapNotificationUM.Error || it is NotificationUM.Error || it is SwapNotificationUM.Warning.ExpressError || it is SwapNotificationUM.Warning.ExpressGeneralError || it is SwapNotificationUM.Warning.NoAvailableTokensToSwap || it is SwapNotificationUM.Warning.NeedReserveToCreateAccount || it is SwapNotificationUM.Info.PermissionNeeded } - MultilineLambdaItParameter:StateBuilder.kt$StateBuilder${ it.convertToProviderBottomSheetState( pricesLowerBest = pricesLowerBest, onProviderSelect = actions.onProviderSelect, needApplyFCARestrictions = needApplyFCARestrictions, ) } - MultilineLambdaItParameter:StateBuilder.kt$StateBuilder${ val selectedItem = when (it) { FeeType.NORMAL -> txFeeState.normalFee FeeType.PRIORITY -> txFeeState.priorityFee } actions.onSelectFeeType.invoke(selectedItem) } - MultilineLambdaItParameter:StateBuilder.kt$StateBuilder${ val tokenInfo = tokenSwapInfoForProviders[it.id] if (it is ProviderState.Content && tokenInfo != null) { val rateString = tokenInfo.tokenAmount .getFormattedCryptoAmount(tokenInfo.cryptoCurrencyStatus.currency) it.copy( subtitle = stringReference(rateString), percentLowerThenBest = pricesLowerBest[it.id]?.let { percent -> PercentDifference.Value(percent) } ?: PercentDifference.Value(0f), ) } else { it } } - MultilineLambdaItParameter:StateBuilder.kt$StateBuilder${ when (it) { is TokenToSelectState.TokenToSelect -> { it.copy( addedTokenBalanceData = it.addedTokenBalanceData?.copy(isBalanceHidden = isBalanceHidden), ) } is TokenToSelectState.Title -> { it } } } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ AccountCryptoCurrencyStatus( account = it.account, status = it.cryptoCurrencyStatus, ) } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ Timber.d("${coin.id} balance is ${it.value.amount}") dataState = dataState.copy( feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( userWalletId = userWalletId, cryptoCurrencyStatus = it, ).getOrNull() ?: it, ) uiState = if (isFromCurrency) { dataState = dataState.copy(fromCryptoCurrency = it) stateBuilder.updateSendCurrencyBalance(uiState, it) } else { dataState = dataState.copy(toCryptoCurrency = it) stateBuilder.updateReceiveCurrencyBalance(uiState, it) } startLoadingQuotesFromLastState(isSilent = true) } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ Timber.e("Error when loading quotes: $it") uiState = stateBuilder.addNotification(uiState, null) { startLoadingQuotesFromLastState() } } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ Timber.e(it) applyInitialTokenChoice( state = TokensDataStateExpress.EMPTY, selectedCurrency = null, selectedAccount = null, isReverseFromTo = isReverseFromTo, ) uiState = stateBuilder.createInitialErrorState( uiState, (it as? ExpressException)?.expressDataError?.code ?: ExpressDataError.UnknownError.code, ) { uiState = stateBuilder.createInitialLoadingState( initialCurrencyFrom = initialCurrencyFrom, initialCurrencyTo = initialCurrencyTo, fromNetworkInfo = initialCurrencyFrom.getNetworkInfo(), ) initTokens(isReverseFromTo) } } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ Timber.e(it) startLoadingQuotesFromLastState() makeDefaultAlert() } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ Timber.e(it.message.orEmpty()) makeDefaultAlert() } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ getAccountCurrencyStatusUseCase.invokeSync( userWalletId = userWalletId, currency = it, ).getOrNull() } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync( userWalletId = userWalletId, cryptoCurrencyId = it.id, ).getOrNull() } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ if (!it.value.fromTokenInfo.amountFiat.isNullOrZero() && !it.value.toTokenInfo.amountFiat.isNullOrZero()) { it.value.fromTokenInfo.amountFiat.divide( it.value.toTokenInfo.amountFiat, it.value.toTokenInfo.cryptoCurrencyStatus.currency.decimals, RoundingMode.HALF_UP, ) } else { BigDecimal.ZERO } } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ if (it.key != selectedProviderEntry.key) { val amount = it.value.toTokenInfo.tokenAmount.value val percentDiff = BigDecimal.ONE.minus( selectedProviderRate.divide(amount, RoundingMode.HALF_UP), ).multiply(hundredPercent) it.key.providerId to percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat() } else { null } } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ isBalanceHidden = it.isBalanceHidden uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden) } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ it is SwapState.SwapError && ( it.error is ExpressDataError.ExchangeTooSmallAmountError || it.error is ExpressDataError.ExchangeTooBigAmountError ) } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ it.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || it.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ subscribeToCoinBalanceUpdates( userWalletId = userWalletId, coin = it, isFromCurrency = false, ) } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ subscribeToCoinBalanceUpdates( userWalletId = userWalletId, coin = it, isFromCurrency = true, ) } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ uiState = stateBuilder.dismissBottomSheet(uiState) dataState = dataState.copy(selectedFee = it) modelScope.launch(dispatchers.io) { startLoadingQuotesFromLastState(false) } } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ val balance = swapInteractor.getTokenBalance(it) onAmountChanged(balance.formatToUIRepresentation()) } - MultilineLambdaItParameter:SwapModel.kt$SwapModel${ val provider = findAndSelectProvider(it) val swapState = dataState.lastLoadedSwapStates[provider] val fromToken = dataState.fromCryptoCurrency if (provider != null && swapState != null && fromToken != null) { analyticsEventHandler.send(SwapEvents.ProviderChosen(provider)) uiState = stateBuilder.dismissBottomSheet(uiState) setupLoadedState( provider = provider, state = swapState, fromToken = fromToken, ) } } - MultilineLambdaItParameter:TransactionCard.kt${ Text( text = it, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, ) } - MultilineLambdaItParameter:TransactionCard.kt${ Text( text = it, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, modifier = Modifier .align(Alignment.CenterVertically) .testTag(SwapTokenScreenTestTags.BALANCE), ) } - MultilineLambdaItParameter:TransactionCard.kt${ Text( text = it, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20), ) } - NamedArguments:SwapModel.kt$SwapModel$PeriodicTask( UPDATE_DELAY, task = { uiState = stateBuilder.createSilentLoadState(uiState) runCatching(dispatchers.io) { dataState = dataState.copy( amount = amount, reduceBalanceBy = reduceBalanceBy, swapDataModel = null, approveDataModel = null, ) swapInteractor.findBestQuote( fromToken = fromToken, fromAccount = fromAccount, toToken = toToken, toAccount = toAccount, providers = toProvidersList, amountToSwap = amount, reduceBalanceBy = reduceBalanceBy, selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL, ) } }, onSuccess = { providersState -> if (providersState.isNotEmpty()) { val (provider, state) = updateLoadedQuotes(providersState) setupLoadedState(provider, state, fromToken) val successStates = providersState.getLastLoadedSuccessStates() val pricesLowerBest = getPricesLowerBest(provider.providerId, successStates) uiState = stateBuilder.updateProvidersBottomSheetContent( uiState = uiState, pricesLowerBest = pricesLowerBest, tokenSwapInfoForProviders = successStates.entries .associate { it.key.providerId to it.value.toTokenInfo }, ) } else { Timber.e("Accidentally empty quotes list") } }, onError = { Timber.e("Error when loading quotes: $it") uiState = stateBuilder.addNotification(uiState, null) { startLoadingQuotesFromLastState() } }, ) - 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() - PropertyUsedBeforeDeclaration:SwapModel.kt$SwapModel$isAccountsMode - SuspendFunSwallowedCancellation:SwapModel.kt$SwapModel$runCatching - UnnecessaryEventHandlerParameter:ChooseFeeBottomSheet.kt$onReadMoreClick: (String) -> Unit - UnnecessaryLet:SwapModel.kt$SwapModel$let { return nonEmptyStates.entries.first { it.key == selectedSwapProvider }.toPair() } - UnusedImports:TransactionCard.kt$import com.tangem.domain.models.account.Account - UseEmptyCounterpart:StoriesEvents.kt$StoriesEvents$mapOf() - UseEmptyCounterpart:SwapEvents.kt$SwapEvents$mapOf() - UseOrEmpty:StateBuilder.kt$StateBuilder$initialCurrencyTo?.symbol ?: "" - UseOrEmpty:SwapModel.kt$SwapModel$receiveToken ?: "" - UseOrEmpty:SwapScreenContent.kt$swapCardState.tokenIconUrl ?: "" - VarCouldBeVal:SwapModel.kt$SwapModel$private var swapRouter: SwapRouter = SwapRouter(router = router) - - diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/StoriesEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/StoriesEvents.kt index b7f89e9288..1174a84ccc 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/StoriesEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/StoriesEvents.kt @@ -6,7 +6,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.WATCHED sealed class StoriesEvents( event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent("Stories", event, params) { data class SwapStories( 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 96e04a99e6..92e8672bb2 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 @@ -15,7 +15,7 @@ private const val PROMO_CATEGORY = "Promo" sealed class SwapEvents( event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent(SWAP_CATEGORY, event, params) { data class SwapScreenOpened(val token: String) : SwapEvents( @@ -23,17 +23,17 @@ 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( + data class ChooseTokenScreenOpened(val hasAvailableTokens: Boolean) : SwapEvents( event = "Choose Token Screen Opened", - params = mapOf("Available tokens" to if (availableTokens) "Yes" else "No"), + params = mapOf("Available tokens" to if (hasAvailableTokens) "Yes" else "No"), ) - data class ChooseTokenScreenResult(val tokenChosen: Boolean, val token: String? = null) : SwapEvents( + data class ChooseTokenScreenResult(val isTokenChosen: Boolean, val token: String? = null) : SwapEvents( event = "Choose Token Screen Result", params = buildMap { - put("Token Chosen", if (tokenChosen) "Yes" else "No") + put("Token Chosen", if (isTokenChosen) "Yes" else "No") token?.let { put("Token", it) } }, ) @@ -71,10 +71,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 @@ -82,6 +83,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( @@ -91,10 +94,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", @@ -111,7 +115,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/converters/AccountTokenItemConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt index f556cdf5e5..4315d780ac 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt @@ -45,11 +45,11 @@ internal class AccountTokenItemConverter( fun createAvailableItemConverter(): TokenItemStateConverter { return TokenItemStateConverter( appCurrency = appCurrency, - subtitleStateProvider = { + subtitleStateProvider = { status -> createSubtitleState( - status = it, + status = status, isAvailable = true, - text = stringReference(value = it.currency.symbol), + text = stringReference(value = status.currency.symbol), ) }, subtitle2StateProvider = ::createSubtitle2State, @@ -64,15 +64,15 @@ internal class AccountTokenItemConverter( return TokenItemStateConverter( appCurrency = appCurrency, iconStateProvider = { CryptoCurrencyToIconStateConverter(isAvailable = false).convert(it) }, - titleStateProvider = { + titleStateProvider = { status -> TokenItemState.TitleState.Content( - text = stringReference(value = it.currency.name), + text = stringReference(value = status.currency.name), isAvailable = false, ) }, - subtitleStateProvider = { + subtitleStateProvider = { status -> createSubtitleState( - status = it, + status = status, isAvailable = false, text = unavailableErrorText, ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index e9f9ec7e68..ba936978ec 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -58,7 +58,7 @@ internal class TokensDataConverter( onSearchEntered = onSearchEntered, onTokenSelected = onTokenSelected, isBalanceHidden = isBalanceHiddenProvider(), - afterSearch = group.isAfterSearch, + isAfterSearch = group.isAfterSearch, ) } @@ -71,7 +71,7 @@ internal class TokensDataConverter( id = cryptoCurrencyStatus.currency.id.value, name = cryptoCurrencyStatus.currency.name, symbol = cryptoCurrencyStatus.currency.symbol, - available = isAvailable, + isAvailable = isAvailable, tokenIcon = convertIcon(cryptoCurrencyStatus.currency, isAvailable), addedTokenBalanceData = TokenBalanceData( amount = formatCryptoAmount(cryptoCurrencyStatus), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt index 3dbd07cda3..90dd691aaa 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt @@ -60,7 +60,7 @@ internal class TokensDataConverterV2( onSearchEntered = onSearchEntered, onTokenSelected = onTokenSelected, isBalanceHidden = isBalanceHidden, - afterSearch = tokensDataState.isAfterSearch, + isAfterSearch = tokensDataState.isAfterSearch, ), ) } 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 649b97059f..f4b0a59776 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 @@ -139,6 +139,7 @@ internal class SwapModel @Inject constructor( private var initialToStatus: CryptoCurrencyStatus? = null private var isBalanceHidden = true + private var isAccountsMode: Boolean = false private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() @@ -151,7 +152,10 @@ internal class SwapModel @Inject constructor( ) private val inputNumberFormatter = - InputNumberFormatter(NumberFormat.getInstance(Locale.getDefault()) as DecimalFormat) + InputNumberFormatter( + NumberFormat.getInstance(Locale.getDefault()) as? DecimalFormat + ?: error("NumberFormat is not DecimalFormat"), + ) private val amountDebouncer = Debouncer() private val singleTaskScheduler = SingleTaskScheduler>() @@ -170,18 +174,17 @@ internal class SwapModel @Inject constructor( private var isOrderReversed = false private val lastAmount = mutableStateOf(INITIAL_AMOUNT) private val lastReducedBalanceBy = mutableStateOf(BigDecimal.ZERO) - private var swapRouter: SwapRouter = SwapRouter(router = router) + private val swapRouter: SwapRouter = SwapRouter(router = router) private var userCountry: UserCountry? = null private lateinit var fromAccountCurrencyStatus: AccountCryptoCurrencyStatus private var toAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null - private var isAccountsMode: Boolean = false - private val isUserResolvableError: (SwapState) -> Boolean = { - it is SwapState.SwapError && + private val isUserResolvableError: (SwapState) -> Boolean = { swapState -> + swapState is SwapState.SwapError && ( - it.error is ExpressDataError.ExchangeTooSmallAmountError || - it.error is ExpressDataError.ExchangeTooBigAmountError + swapState.error is ExpressDataError.ExchangeTooSmallAmountError || + swapState.error is ExpressDataError.ExchangeTooBigAmountError ) } @@ -210,10 +213,10 @@ internal class SwapModel @Inject constructor( userWalletId = userWalletId, currency = initialCurrencyFrom, ).getOrNull() - val toAccountStatus = initialCurrencyTo?.let { + val toAccountStatus = initialCurrencyTo?.let { currencyTo -> getAccountCurrencyStatusUseCase.invokeSync( userWalletId = userWalletId, - currency = it, + currency = currencyTo, ).getOrNull() } @@ -228,10 +231,10 @@ internal class SwapModel @Inject constructor( } } else { val fromStatus = getFromStatus() - val toStatus = initialCurrencyTo?.let { + val toStatus = initialCurrencyTo?.let { currencyTo -> getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync( userWalletId = userWalletId, - cryptoCurrencyId = it.id, + cryptoCurrencyId = currencyTo.id, ).getOrNull() } @@ -248,8 +251,8 @@ internal class SwapModel @Inject constructor( analyticsEventHandler.send(SwapEvents.SwapScreenOpened(initialCurrencyFrom.symbol)) getBalanceHidingSettingsUseCase() - .onEach { - isBalanceHidden = it.isBalanceHidden + .onEach { settings -> + isBalanceHidden = settings.isBalanceHidden uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden) } .launchIn(modelScope) @@ -275,7 +278,7 @@ internal class SwapModel @Inject constructor( val isAnyAvailableAccountTokensFrom = !dataState.tokensDataState?.fromGroup?.accountCurrencyList.isNullOrEmpty() val isAnyAvailableTokens = isAnyAvailableTokensTo || isAnyAvailableTokensFrom || isAnyAvailableAccountTokensTo || isAnyAvailableAccountTokensFrom - analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(availableTokens = isAnyAvailableTokens)) + analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(hasAvailableTokens = isAnyAvailableTokens)) } private fun initTokens(isReverseFromTo: Boolean) { @@ -290,10 +293,10 @@ internal class SwapModel @Inject constructor( initialCryptoCurrency = initialCurrencyFrom, state = state, isReverseFromTo = isReverseFromTo, - )?.let { + )?.let { accountSwapCurrency -> AccountCryptoCurrencyStatus( - account = it.account, - status = it.cryptoCurrencyStatus, + account = accountSwapCurrency.account, + status = accountSwapCurrency.cryptoCurrencyStatus, ) } selectedAccountCurrency?.status to selectedAccountCurrency?.account @@ -313,23 +316,23 @@ internal class SwapModel @Inject constructor( isReverseFromTo = isReverseFromTo, ) - (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { + (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { coin -> subscribeToCoinBalanceUpdates( userWalletId = userWalletId, - coin = it, + coin = coin, isFromCurrency = true, ) } - (dataState.toCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { + (dataState.toCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { coin -> subscribeToCoinBalanceUpdates( userWalletId = userWalletId, - coin = it, + coin = coin, isFromCurrency = false, ) } - }.onFailure { - Timber.e(it) + }.onFailure { error -> + Timber.e(error) applyInitialTokenChoice( state = TokensDataStateExpress.EMPTY, @@ -340,7 +343,7 @@ internal class SwapModel @Inject constructor( uiState = stateBuilder.createInitialErrorState( uiState, - (it as? ExpressException)?.expressDataError?.code ?: ExpressDataError.UnknownError.code, + (error as? ExpressException)?.expressDataError?.code ?: ExpressDataError.UnknownError.code, ) { uiState = stateBuilder.createInitialLoadingState( initialCurrencyFrom = initialCurrencyFrom, @@ -376,7 +379,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, @@ -389,7 +392,7 @@ internal class SwapModel @Inject constructor( } else { initialFromStatus to selectedCurrency } - val (fromAccount, toAccount) = if (accountsFeatureToggles.isFeatureEnabled) { + val (fromAccount, toAccount) = if (accountsFeatureToggles.isFeatureEnabled && tangemPayInput == null) { if (isOrderReversed) { selectedAccount to fromAccountCurrencyStatus.account } else { @@ -497,7 +500,7 @@ internal class SwapModel @Inject constructor( toProvidersList: List, ): PeriodicTask> { return PeriodicTask( - UPDATE_DELAY, + delay = UPDATE_DELAY, task = { uiState = stateBuilder.createSilentLoadState(uiState) runCatching(dispatchers.io) { @@ -535,8 +538,8 @@ internal class SwapModel @Inject constructor( Timber.e("Accidentally empty quotes list") } }, - onError = { - Timber.e("Error when loading quotes: $it") + onError = { error -> + Timber.e("Error when loading quotes: $error") uiState = stateBuilder.addNotification(uiState, null) { startLoadingQuotesFromLastState() } }, ) @@ -641,13 +644,13 @@ internal class SwapModel @Inject constructor( } private fun sendErrorAnalyticsEvent(error: ExpressDataError, provider: SwapProvider) { - val receiveToken = dataState.toCryptoCurrency?.currency?.let { - "${it.network.backendId}:${it.symbol}" + val receiveToken = dataState.toCryptoCurrency?.currency?.let { currency -> + "${currency.network.backendId}:${currency.symbol}" } analyticsErrorEventHandler.sendErrorEvent( SwapEvents.NoticeProviderError( sendToken = "${initialCurrencyFrom.network.backendId}:${initialCurrencyFrom.symbol}", - receiveToken = receiveToken ?: "", + receiveToken = receiveToken.orEmpty(), provider = provider, errorCode = error.code, errorMessage = error.message, @@ -656,7 +659,7 @@ internal class SwapModel @Inject constructor( } private fun updateLoadedQuotes(state: Map): Pair { - val nonEmptyStates = state.filter { it.value !is SwapState.EmptyAmountState } + val nonEmptyStates = state.filter { entry -> entry.value !is SwapState.EmptyAmountState } val selectedSwapProvider = if (nonEmptyStates.isNotEmpty()) { selectProvider(state) } else { @@ -666,8 +669,8 @@ internal class SwapModel @Inject constructor( selectedProvider = selectedSwapProvider, lastLoadedSwapStates = state, ) - selectedSwapProvider?.let { - return nonEmptyStates.entries.first { it.key == selectedSwapProvider }.toPair() + if (selectedSwapProvider != null) { + return nonEmptyStates.entries.first { entry -> entry.key == selectedSwapProvider }.toPair() } return state.entries.first().toPair() } @@ -832,8 +835,8 @@ internal class SwapModel @Inject constructor( processTangemPayWithdrawal(swapTransactionState = swapTransactionState) } } - }.onFailure { - Timber.e(it) + }.onFailure { error -> + Timber.e(error) startLoadingQuotesFromLastState() makeDefaultAlert() } @@ -889,6 +892,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( @@ -898,6 +903,8 @@ internal class SwapModel @Inject constructor( receiveBlockchain = toCurrency.network.name, sendToken = fromCurrency.symbol, receiveToken = toCurrency.symbol, + fromDerivationIndex = fromDerivationIndex, + toDerivationIndex = toDerivationIndex, ), ) } @@ -905,7 +912,7 @@ internal class SwapModel @Inject constructor( @Suppress("LongMethod") private fun givePermissionsToSwap() { modelScope.launch(dispatchers.main) { - runCatching { + runSuspendCatching { val fromCryptoCurrency = requireNotNull(dataState.fromCryptoCurrency) { "dataState.fromCryptoCurrency might not be null" } @@ -922,7 +929,7 @@ internal class SwapModel @Inject constructor( TxFeeState.Empty -> { makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) Timber.e("Fee should not be Empty") - return@runCatching + return@launch } is TxFeeState.MultipleFeeState -> fee.priorityFee is TxFeeState.SingleFeeState -> fee.fee @@ -969,8 +976,8 @@ internal class SwapModel @Inject constructor( } } }.onFailure { makeDefaultAlert() } - }.onFailure { - Timber.e(it.message.orEmpty()) + }.onFailure { error -> + Timber.e(error.message.orEmpty()) makeDefaultAlert() } } @@ -985,13 +992,13 @@ internal class SwapModel @Inject constructor( tokenDataState.toGroup } - val available = group.available.filter { - it.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || - it.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) + val available = group.available.filter { swapAvailability -> + swapAvailability.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || + swapAvailability.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) } - val unavailable = group.unavailable.filter { - it.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || - it.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) + val unavailable = group.unavailable.filter { swapAvailability -> + swapAvailability.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || + swapAvailability.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) } val accountCurrencyList = group.accountCurrencyList.mapNotNull { accountSwapAvailability -> val filteredCurrencies = accountSwapAvailability.currencyList.filter { accountSwapCurrency -> @@ -1036,8 +1043,8 @@ internal class SwapModel @Inject constructor( val tokens = dataState.tokensDataState ?: return val (foundToken, foundAccount) = getSelectedTokenAndAccount(tokens, id) - foundToken?.currency?.symbol?.let { - analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(tokenChosen = true, token = it)) + foundToken?.currency?.symbol?.let { symbol -> + analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = symbol)) } if (foundToken != null) { @@ -1117,8 +1124,10 @@ internal class SwapModel @Inject constructor( tokens.fromGroup } else { tokens.toGroup - }.accountCurrencyList.firstNotNullOfOrNull { - it.currencyList.firstOrNull { it.cryptoCurrencyStatus.currency.id.value == id } + }.accountCurrencyList.firstNotNullOfOrNull { accountSwapAvailability -> + accountSwapAvailability.currencyList.firstOrNull { accountSwapCurrency -> + accountSwapCurrency.cryptoCurrencyStatus.currency.id.value == id + } } accountCryptoCurrencyStatus?.cryptoCurrencyStatus to accountCryptoCurrencyStatus?.account } else { @@ -1126,7 +1135,9 @@ internal class SwapModel @Inject constructor( tokens.fromGroup } else { tokens.toGroup - }.available.firstOrNull { it.currencyStatus.currency.id.value == id }?.currencyStatus to null + }.available.firstOrNull { swapAvailability -> + swapAvailability.currencyStatus.currency.id.value == id + }?.currencyStatus to null } } @@ -1143,7 +1154,7 @@ internal class SwapModel @Inject constructor( currency = coin, ).distinctUntilChanged { old, new -> old.status.value.amount == new.status.value.amount } // Check only balance changes .onEach { (account, currencyStatus) -> - Timber.d("${coin.id} balance is ${currencyStatus.value.amount}") + Timber.d("${coin.id} balance is ${currencyStatus.value.amount ?: "null"}") dataState = dataState.copy( feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( @@ -1173,24 +1184,24 @@ internal class SwapModel @Inject constructor( userWalletId = userWalletId, currencyId = coin.id, isSingleWalletWithTokens = false, - ).mapNotNull { (it as? Either.Right)?.value } + ).mapNotNull { either -> (either as? Either.Right)?.value } .distinctUntilChanged { old, new -> old.value.amount == new.value.amount } // Check only balance changes - .onEach { - Timber.d("${coin.id} balance is ${it.value.amount}") + .onEach { status -> + Timber.d("${coin.id} balance is ${status.value.amount ?: "null"}") dataState = dataState.copy( feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( userWalletId = userWalletId, - cryptoCurrencyStatus = it, - ).getOrNull() ?: it, + cryptoCurrencyStatus = status, + ).getOrNull() ?: status, ) uiState = if (isFromCurrency) { - dataState = dataState.copy(fromCryptoCurrency = it) - stateBuilder.updateSendCurrencyBalance(uiState, it) + dataState = dataState.copy(fromCryptoCurrency = status) + stateBuilder.updateSendCurrencyBalance(uiState, status) } else { - dataState = dataState.copy(toCryptoCurrency = it) - stateBuilder.updateReceiveCurrencyBalance(uiState, it) + dataState = dataState.copy(toCryptoCurrency = status) + stateBuilder.updateReceiveCurrencyBalance(uiState, status) } startLoadingQuotesFromLastState(isSilent = true) @@ -1217,8 +1228,8 @@ internal class SwapModel @Inject constructor( toAccount = newToAccount, ) isOrderReversed = !isOrderReversed - dataState.tokensDataState?.let { - updateTokensState(it) + dataState.tokensDataState?.let { tokensDataState -> + updateTokensState(tokensDataState) } val minTxAmount = getMinimumTransactionAmountSyncUseCase( @@ -1297,8 +1308,8 @@ internal class SwapModel @Inject constructor( } private fun onMaxAmountClicked() { - dataState.fromCryptoCurrency?.let { - val balance = swapInteractor.getTokenBalance(it) + dataState.fromCryptoCurrency?.let { fromCurrency -> + val balance = swapInteractor.getTokenBalance(fromCurrency) onAmountChanged(balance.formatToUIRepresentation()) } } @@ -1313,7 +1324,7 @@ internal class SwapModel @Inject constructor( private fun onAmountSelected(selected: Boolean) { if (selected) { - analyticsEventHandler.send(SwapEvents.SendTokenBalanceClicked) + analyticsEventHandler.send(SwapEvents.SendTokenBalanceClicked()) } } @@ -1354,7 +1365,7 @@ internal class SwapModel @Inject constructor( }, onChangeCardsClicked = { onChangeCardsClicked() - analyticsEventHandler.send(SwapEvents.ButtonSwipeClicked) + analyticsEventHandler.send(SwapEvents.ButtonSwipeClicked()) }, onBackClicked = { val bottomSheet = uiState.bottomSheetConfig @@ -1362,7 +1373,7 @@ internal class SwapModel @Inject constructor( uiState = stateBuilder.dismissBottomSheet(uiState) } else { if (swapRouter.currentScreen == SwapNavScreen.SelectToken) { - analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(tokenChosen = false)) + analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(isTokenChosen = false)) } swapRouter.back() } @@ -1376,7 +1387,7 @@ internal class SwapModel @Inject constructor( sendGivePermissionClickedEvent() uiState = stateBuilder.showPermissionBottomSheet(uiState) { startLoadingQuotesFromLastState(isSilent = true) - analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked) + analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked()) uiState = stateBuilder.dismissBottomSheet(uiState) } }, @@ -1396,15 +1407,15 @@ internal class SwapModel @Inject constructor( uiState = stateBuilder.dismissBottomSheet(uiState) } }, - onSelectFeeType = { + onSelectFeeType = { feeType -> uiState = stateBuilder.dismissBottomSheet(uiState) - dataState = dataState.copy(selectedFee = it) + dataState = dataState.copy(selectedFee = feeType) modelScope.launch(dispatchers.io) { startLoadingQuotesFromLastState(false) } }, onProviderClick = { providerId -> - analyticsEventHandler.send(SwapEvents.ProviderClicked) + analyticsEventHandler.send(SwapEvents.ProviderClicked()) val states = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates() val pricesLowerBest = getPricesLowerBest(providerId, states) uiState = stateBuilder.showSelectProviderBottomSheet( @@ -1415,8 +1426,8 @@ internal class SwapModel @Inject constructor( needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), ) { uiState = stateBuilder.dismissBottomSheet(uiState) } }, - onProviderSelect = { - val provider = findAndSelectProvider(it) + onProviderSelect = { providerId -> + val provider = findAndSelectProvider(providerId) val swapState = dataState.lastLoadedSwapStates[provider] val fromToken = dataState.fromCryptoCurrency if (provider != null && swapState != null && fromToken != null) { @@ -1516,11 +1527,14 @@ internal class SwapModel @Inject constructor( private fun findBestQuoteProvider(state: SuccessLoadedSwapData): SwapProvider? { // finding best quotes - return state.minByOrNull { - if (!it.value.fromTokenInfo.amountFiat.isNullOrZero() && !it.value.toTokenInfo.amountFiat.isNullOrZero()) { - it.value.fromTokenInfo.amountFiat.divide( - it.value.toTokenInfo.amountFiat, - it.value.toTokenInfo.cryptoCurrencyStatus.currency.decimals, + return state.minByOrNull { entry -> + val toTokenInfo = entry.value.toTokenInfo + val fromAmountFiat = entry.value.fromTokenInfo.amountFiat + val toAmountFiat = toTokenInfo.amountFiat + if (!fromAmountFiat.isNullOrZero() && !toAmountFiat.isNullOrZero()) { + fromAmountFiat.divide( + toAmountFiat, + toTokenInfo.cryptoCurrencyStatus.currency.decimals, RoundingMode.HALF_UP, ) } else { @@ -1530,17 +1544,19 @@ internal class SwapModel @Inject constructor( } private fun getPricesLowerBest(selectedProviderId: String, state: SuccessLoadedSwapData): Map { - val selectedProviderEntry = state.filter { it.key.providerId == selectedProviderId }.entries.firstOrNull() - ?: return emptyMap() + val selectedProviderEntry = state + .filter { entry -> entry.key.providerId == selectedProviderId } + .entries + .firstOrNull() ?: return emptyMap() val selectedProviderRate = selectedProviderEntry.value.toTokenInfo.tokenAmount.value val hundredPercent = BigDecimal("100") - return state.entries.mapNotNull { - if (it.key != selectedProviderEntry.key) { - val amount = it.value.toTokenInfo.tokenAmount.value + return state.entries.mapNotNull { entry -> + if (entry.key != selectedProviderEntry.key) { + val amount = entry.value.toTokenInfo.tokenAmount.value val percentDiff = BigDecimal.ONE.minus( selectedProviderRate.divide(amount, RoundingMode.HALF_UP), ).multiply(hundredPercent) - it.key.providerId to percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat() + entry.key.providerId to percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat() } else { null } @@ -1572,11 +1588,15 @@ internal class SwapModel @Inject constructor( return if (accountsFeatureToggles.isFeatureEnabled) { groupToFind.accountCurrencyList.firstNotNullOfOrNull { (_, currencyList) -> - currencyList.find { idToFind == it.cryptoCurrencyStatus.currency.id.value && it.isAvailable } + currencyList.find { accountSwapCurrency -> + idToFind == accountSwapCurrency.cryptoCurrencyStatus.currency.id.value && + accountSwapCurrency.isAvailable + } }?.providers } else { - groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value } - ?.providers + groupToFind.available.find { swapAvailability -> + idToFind == swapAvailability.currencyStatus.currency.id.value + }?.providers } ?.filterForTangemPayWithdrawal() .orEmpty() @@ -1591,13 +1611,13 @@ internal class SwapModel @Inject constructor( } private fun Map.getLastLoadedSuccessStates(): SuccessLoadedSwapData { - return this.filter { it.value is SwapState.QuotesLoadedState } - .mapValues { it.value as SwapState.QuotesLoadedState } + return this.filter { entry -> entry.value is SwapState.QuotesLoadedState } + .mapValues { entry -> entry.value as SwapState.QuotesLoadedState } } private fun Map.consideredProvidersStates(): Map { - return this.filter { - it.value is SwapState.QuotesLoadedState || isUserResolvableError(it.value) + return this.filter { entry -> + entry.value is SwapState.QuotesLoadedState || isUserResolvableError(entry.value) } } @@ -1615,10 +1635,14 @@ internal class SwapModel @Inject constructor( val chosen = if (isOrderReversed) from else to return if (accountsFeatureToggles.isFeatureEnabled) { - currenciesGroup.accountCurrencyList.flatMap { it.currencyList.map { it.cryptoCurrencyStatus } } + currenciesGroup.accountCurrencyList.flatMap { accountSwapAvailability -> + accountSwapAvailability.currencyList.map { accountSwapCurrency -> + accountSwapCurrency.cryptoCurrencyStatus + } + } } else { - currenciesGroup.available.map { it.currencyStatus } - }.map { it.currency }.contains(chosen.currency) + currenciesGroup.available.map { swapAvailability -> swapAvailability.currencyStatus } + }.map { currencyStatus -> currencyStatus.currency }.contains(chosen.currency) } private fun sendNoticePermissionNeededEvent() { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index 9c60150644..731e036f94 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -125,9 +125,10 @@ internal class SwapNotificationsFactory( if (quoteModel.permissionState is PermissionDataState.PermissionLoading) { add(SwapNotificationUM.Error.ApprovalInProgressWarning) } else if (quoteModel.preparedSwapConfigState.hasOutgoingTransaction) { + val fromCurrency = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency add( SwapNotificationUM.Error.TransactionInProgressWarning( - currencySymbol = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency.network.currencySymbol, + currencySymbol = fromCurrency.network.currencySymbol, ), ) } @@ -290,10 +291,10 @@ internal class SwapNotificationsFactory( ) { if (hideFee) return val feeEnoughState = quoteModel.preparedSwapConfigState.feeState as? SwapFeeState.NotEnough ?: return - val needShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough && + val shouldShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough && quoteModel.permissionState !is PermissionDataState.PermissionLoading && feeEnoughState.feeCurrency != fromToken - if (needShowCoverWarning) { + if (shouldShowCoverWarning) { add( SwapNotificationUM.Error.UnableToCoverFeeWarning( fromToken = fromToken, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt index aab8a21eae..8d9c1bf701 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt @@ -11,7 +11,7 @@ internal data class SwapSelectTokenStateHolder( val unavailableTokens: ImmutableList, val tokensListData: TokenListUMData, val isBalanceHidden: Boolean, - val afterSearch: Boolean, + val isAfterSearch: Boolean, val onSearchEntered: (String) -> Unit, val onTokenSelected: (String) -> Unit, ) @@ -25,7 +25,7 @@ internal sealed class TokenToSelectState { val name: String, val symbol: String, val tokenIcon: CurrencyIconState, - val available: Boolean = true, + val isAvailable: Boolean = true, val addedTokenBalanceData: TokenBalanceData? = null, ) : TokenToSelectState() } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 43985f8975..48a1a717c6 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -77,7 +77,7 @@ sealed class SwapCardState { data class SwapButton( @DrawableRes val walletInteractionIcon: Int?, - val enabled: Boolean, + val isEnabled: Boolean, val onClick: () -> Unit, ) @@ -94,7 +94,7 @@ sealed interface TransactionCardType { ) : TransactionCardType data class ReadOnly( - val showWarning: Boolean = false, + val shouldShowWarning: Boolean = false, val onWarningClick: (() -> Unit)? = null, override val inputError: InputError = InputError.Empty, override val accountTitleUM: AccountTitleUM? = null, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt index 906782071d..5156ce3e1e 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt @@ -9,7 +9,7 @@ data class SwapSuccessStateHolder( val txUrl: String, val fee: TextReference, val rate: TextReference, - val showStatusButton: Boolean, + val shouldShowStatusButton: Boolean, val providerName: TextReference, val providerType: TextReference, val providerIcon: String, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/preview/SwapSuccessStatePreview.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/preview/SwapSuccessStatePreview.kt index 9c3b0ea54d..77c343f43b 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/preview/SwapSuccessStatePreview.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/preview/SwapSuccessStatePreview.kt @@ -17,7 +17,7 @@ internal data object SwapSuccessStatePreview { fee = TextReference.Str("1 000 DAI ~ 1 000 MATIC"), providerName = TextReference.Str("1inch"), providerType = TextReference.Str(ExchangeProviderType.DEX.providerName), - showStatusButton = false, + shouldShowStatusButton = false, providerIcon = "", fromTitle = AccountTitleUM.Account( prefixText = stringReference("From"), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt index 3946ea9358..067df047a5 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt @@ -58,14 +58,13 @@ private fun ChooseFeeBottomSheetContent(content: ChooseFeeBottomSheetConfig) { } FooterBlock( readMore = content.readMore, - readMoreUrl = content.readMoreUrl, - onReadMoreClick = content.onReadMoreClick, + onReadMoreClick = { content.onReadMoreClick(content.readMoreUrl) }, ) } } @Composable -private fun FooterBlock(readMore: TextReference, readMoreUrl: String, onReadMoreClick: (String) -> Unit) { +private fun FooterBlock(readMore: TextReference, onReadMoreClick: () -> Unit) { val linkText = readMore.resolveReference() val fullString = stringResourceSafe(R.string.common_fee_selector_footer, linkText) val linkTextPosition = fullString.length - linkText.length @@ -81,7 +80,7 @@ private fun FooterBlock(readMore: TextReference, readMoreUrl: String, onReadMore val click = { i: Int -> val readMoreStyle = requireNotNull(annotatedString.spanStyles.getOrNull(1)) if (i in readMoreStyle.start..readMoreStyle.end) { - onReadMoreClick(readMoreUrl) + onReadMoreClick() } } @@ -102,7 +101,7 @@ private fun FooterBlock(readMore: TextReference, readMoreUrl: String, onReadMore private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { content.feeItems.forEachIndexed { index, feeItem -> val isSelected = feeItem.feeType == content.selectedFee - val showDivider = content.feeItems.lastIndex != index + val shouldShowDivider = content.feeItems.lastIndex != index val symbol = " ${feeItem.symbolCrypto}" val preDotText = "${feeItem.amountCrypto}$symbol" val postDot = feeItem.amountFiatFormatted @@ -117,7 +116,7 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { ellipsizeOffset = ellipsizeOffset, isSelected = isSelected, onSelect = { content.onSelectFeeType(feeItem.feeType) }, - showDivider = showDivider, + showDivider = shouldShowDivider, ) } FeeType.PRIORITY -> { @@ -129,7 +128,7 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { ellipsizeOffset = ellipsizeOffset, isSelected = isSelected, onSelect = { content.onSelectFeeType(feeItem.feeType) }, - showDivider = showDivider, + showDivider = shouldShowDivider, ) } } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index ac6ebfd1b3..a610eaac34 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -132,16 +132,16 @@ private fun ProviderContentState( modifier = Modifier.padding(end = TangemTheme.dimens.spacing4), ) } - AnimatedContent(targetState = state.name, label = "") { + AnimatedContent(targetState = state.name, label = "") { name -> Text( - text = it, + text = name, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.primary1, ) } - AnimatedContent(targetState = state.type, label = "") { + AnimatedContent(targetState = state.type, label = "") { type -> Text( - text = it, + text = type, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), @@ -162,9 +162,9 @@ private fun ProviderContentState( end = TangemTheme.dimens.spacing56, ), ) { - AnimatedContent(targetState = state.subtitle, label = "") { + AnimatedContent(targetState = state.subtitle, label = "") { subtitle -> Text( - text = it.resolveReference(), + text = subtitle.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, overflow = TextOverflow.Ellipsis, @@ -179,9 +179,9 @@ private fun ProviderContentState( } else { TangemTheme.colors.text.warning } - AnimatedContent(targetState = state.percentLowerThenBest.value, label = "") { + AnimatedContent(targetState = state.percentLowerThenBest.value, label = "") { percentValue -> Text( - text = if (it > 0) "+$it%" else "$it%", + text = if (percentValue > 0) "+$percentValue%" else "$percentValue%", style = TangemTheme.typography.body2, color = textColor, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), @@ -231,25 +231,25 @@ private fun ProviderUnavailableState( modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), ) { Row { - AnimatedContent(targetState = state.name, label = "") { + AnimatedContent(targetState = state.name, label = "") { name -> Text( - text = it, + text = name, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) } - AnimatedContent(targetState = state.type, label = "") { + AnimatedContent(targetState = state.type, label = "") { type -> Text( - text = it, + text = type, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), ) } } - AnimatedContent(targetState = state.alertText, label = "") { + AnimatedContent(targetState = state.alertText, label = "") { alertText -> Text( - text = it.resolveReference(), + text = alertText.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(top = TangemTheme.dimens.spacing6), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 6369135b49..64ac9aa044 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -109,7 +109,7 @@ internal class StateBuilder( type = TransactionCardType.ReadOnly(), amountEquivalent = null, tokenIconUrl = initialCurrencyTo?.iconUrl, - tokenCurrency = initialCurrencyTo?.symbol ?: "", + tokenCurrency = initialCurrencyTo?.symbol.orEmpty(), token = null, amountTextFieldValue = null, canSelectAnotherToken = false, @@ -122,7 +122,7 @@ internal class StateBuilder( fee = FeeItemState.Empty, swapButton = SwapButton( walletInteractionIcon = walletInterationIcon(userWalletProvider()), - enabled = false, + isEnabled = false, onClick = {}, ), onRefresh = {}, @@ -181,7 +181,7 @@ internal class StateBuilder( fee = FeeItemState.Empty, swapButton = SwapButton( walletInteractionIcon = walletInterationIcon(userWalletProvider()), - enabled = false, + isEnabled = false, onClick = { }, ), changeCardsButtonState = ChangeCardsButtonState.DISABLED, @@ -246,7 +246,7 @@ internal class StateBuilder( fee = FeeItemState.Empty, swapButton = SwapButton( walletInteractionIcon = walletInterationIcon(userWalletProvider()), - enabled = false, + isEnabled = false, onClick = {}, ), providerState = ProviderState.Loading(), @@ -335,7 +335,7 @@ internal class StateBuilder( ), receiveCardData = SwapCardState.SwapCardData( type = TransactionCardType.ReadOnly( - showWarning = true, + shouldShowWarning = true, onWarningClick = actions.onReceiveCardWarningClick, accountTitleUM = getToCardAccountTitle(toAccount), ), @@ -368,7 +368,7 @@ internal class StateBuilder( fee = feeState, swapButton = SwapButton( walletInteractionIcon = walletInterationIcon(userWalletProvider()), - enabled = getSwapButtonEnabled(notifications), + isEnabled = getSwapButtonEnabled(notifications), onClick = actions.onSwapClick, ), changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), @@ -397,17 +397,17 @@ internal class StateBuilder( private fun createTosState(swapProvider: SwapProvider): TosState { return TosState( - tosLink = swapProvider.termsOfUse?.let { + tosLink = swapProvider.termsOfUse?.let { termsUrl -> LegalState( title = resourceReference(R.string.common_terms_of_use), - link = it, + link = termsUrl, onClick = actions.onLinkClick, ) }, - policyLink = swapProvider.privacyPolicy?.let { + policyLink = swapProvider.privacyPolicy?.let { policyUrl -> LegalState( title = resourceReference(R.string.common_privacy_policy), - link = it, + link = policyUrl, onClick = actions.onLinkClick, ) }, @@ -420,12 +420,13 @@ internal class StateBuilder( } private fun getSwapButtonEnabled(notifications: ImmutableList): Boolean { - return notifications.none { - it is SwapNotificationUM.Error || it is NotificationUM.Error || - it is SwapNotificationUM.Warning.ExpressError || it is SwapNotificationUM.Warning.ExpressGeneralError || - it is SwapNotificationUM.Warning.NoAvailableTokensToSwap || - it is SwapNotificationUM.Warning.NeedReserveToCreateAccount || - it is SwapNotificationUM.Info.PermissionNeeded + return notifications.none { notification -> + notification is SwapNotificationUM.Error || notification is NotificationUM.Error || + notification is SwapNotificationUM.Warning.ExpressError || + notification is SwapNotificationUM.Warning.ExpressGeneralError || + notification is SwapNotificationUM.Warning.NoAvailableTokensToSwap || + notification is SwapNotificationUM.Warning.NeedReserveToCreateAccount || + notification is SwapNotificationUM.Info.PermissionNeeded } } @@ -495,7 +496,7 @@ internal class StateBuilder( fee = FeeItemState.Empty, swapButton = SwapButton( walletInteractionIcon = walletInterationIcon(userWalletProvider()), - enabled = false, + isEnabled = false, onClick = actions.onSwapClick, ), changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), @@ -592,7 +593,7 @@ internal class StateBuilder( fee = FeeItemState.Empty, swapButton = SwapButton( walletInteractionIcon = walletInterationIcon(userWalletProvider()), - enabled = false, + isEnabled = false, onClick = { }, ), changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), @@ -604,7 +605,7 @@ internal class StateBuilder( fun createSwapInProgressState(uiState: SwapStateHolder): SwapStateHolder { return uiState.copy( swapButton = uiState.swapButton.copy( - enabled = false, + isEnabled = false, ), ) } @@ -722,15 +723,17 @@ internal class StateBuilder( ) val selectTokenState = uiState.selectTokenState?.copy( isBalanceHidden = isBalanceHidden, - availableTokens = uiState.selectTokenState.availableTokens.map { - when (it) { + availableTokens = uiState.selectTokenState.availableTokens.map { tokenState -> + when (tokenState) { is TokenToSelectState.TokenToSelect -> { - it.copy( - addedTokenBalanceData = it.addedTokenBalanceData?.copy(isBalanceHidden = isBalanceHidden), + tokenState.copy( + addedTokenBalanceData = tokenState.addedTokenBalanceData?.copy( + isBalanceHidden = isBalanceHidden, + ), ) } is TokenToSelectState.Title -> { - it + tokenState } } }.toImmutableList(), @@ -804,7 +807,7 @@ internal class StateBuilder( fun loadingPermissionState(uiState: SwapStateHolder): SwapStateHolder { return uiState.copy( swapButton = uiState.swapButton.copy( - enabled = false, + isEnabled = false, ), permissionState = GiveTxPermissionState.InProgress, notifications = notificationsFactory.getApprovalInProgressStateNotification(uiState.notifications), @@ -837,7 +840,7 @@ internal class StateBuilder( txUrl = txUrl, providerName = stringReference(providerState.name), providerType = stringReference(providerState.type), - showStatusButton = shouldShowStatus, + shouldShowStatusButton = shouldShowStatus, providerIcon = providerState.iconUrl, rate = providerState.subtitle, fee = stringReference("${fee.feeCryptoFormattedWithNative} (${fee.feeFiatFormattedWithNative})"), @@ -877,7 +880,7 @@ internal class StateBuilder( txUrl = txUrl, providerName = stringReference(providerState.name), providerType = stringReference(providerState.type), - showStatusButton = false, + shouldShowStatusButton = false, providerIcon = providerState.iconUrl, rate = providerState.subtitle, fee = TextReference.EMPTY, @@ -1101,8 +1104,8 @@ internal class StateBuilder( onDismiss: () -> Unit, ): SwapStateHolder { val availableProvidersStates = providersStates.entries - .mapNotNull { - it.convertToProviderBottomSheetState( + .mapNotNull { entry -> + entry.convertToProviderBottomSheetState( pricesLowerBest = pricesLowerBest, onProviderSelect = actions.onProviderSelect, needApplyFCARestrictions = needApplyFCARestrictions, @@ -1139,19 +1142,19 @@ internal class StateBuilder( uiState.copy( bottomSheetConfig = uiState.bottomSheetConfig.copy( content = config.copy( - providers = providers.map { - val tokenInfo = tokenSwapInfoForProviders[it.id] - if (it is ProviderState.Content && tokenInfo != null) { + providers = providers.map { providerState -> + val tokenInfo = tokenSwapInfoForProviders[providerState.id] + if (providerState is ProviderState.Content && tokenInfo != null) { val rateString = tokenInfo.tokenAmount .getFormattedCryptoAmount(tokenInfo.cryptoCurrencyStatus.currency) - it.copy( + providerState.copy( subtitle = stringReference(rateString), - percentLowerThenBest = pricesLowerBest[it.id]?.let { percent -> + percentLowerThenBest = pricesLowerBest[providerState.id]?.let { percent -> PercentDifference.Value(percent) } ?: PercentDifference.Value(0f), ) } else { - it + providerState } }.toImmutableList(), ), @@ -1170,8 +1173,8 @@ internal class StateBuilder( ): SwapStateHolder { val config = ChooseFeeBottomSheetConfig( selectedFee = selectedFee, - onSelectFeeType = { - val selectedItem = when (it) { + onSelectFeeType = { feeType -> + val selectedItem = when (feeType) { FeeType.NORMAL -> txFeeState.normalFee FeeType.PRIORITY -> txFeeState.priorityFee } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 486c515d86..3aa2c4d059 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -175,7 +175,7 @@ private fun TransactionCardData( balance = swapCardState.balance.orMaskWithStars(swapCardState.isBalanceHidden), textFieldValue = swapCardState.amountTextFieldValue, amountEquivalent = swapCardState.amountEquivalent, - tokenIconUrl = swapCardState.tokenIconUrl ?: "", + tokenIconUrl = swapCardState.tokenIconUrl.orEmpty(), tokenCurrency = swapCardState.tokenCurrency, priceImpact = priceImpact, networkIconRes = if (swapCardState.isNotNativeToken) swapCardState.networkIconRes else null, @@ -374,7 +374,7 @@ private fun MainButton(state: SwapStateHolder) { modifier = Modifier.fillMaxWidth(), text = stringResourceSafe(id = R.string.swapping_swap_action), iconResId = state.swapButton.walletInteractionIcon, - enabled = state.swapButton.enabled, + enabled = state.swapButton.isEnabled, onClick = state.swapButton.onClick, ) } @@ -437,7 +437,7 @@ private val state = SwapStateHolder( ), SwapNotificationUM.Warning.NoAvailableTokensToSwap("POLYGON"), ), - swapButton = SwapButton(enabled = true, onClick = {}, walletInteractionIcon = null), + swapButton = SwapButton(isEnabled = true, onClick = {}, walletInteractionIcon = null), onRefresh = {}, onBackClicked = {}, onChangeCardsClicked = {}, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 39625f9e1f..6b0fa946f4 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -57,11 +57,11 @@ internal fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () val modifier = Modifier.padding(padding) when { state.availableTokens.isEmpty() && state.unavailableTokens.isEmpty() && - state.tokensListData.tokensList.isEmpty() && state.afterSearch -> { + state.tokensListData.tokensList.isEmpty() && state.isAfterSearch -> { TokensNotFound(modifier) } state.availableTokens.isEmpty() && state.unavailableTokens.isEmpty() && - state.tokensListData.tokensList.isEmpty() && !state.afterSearch -> { + state.tokensListData.tokensList.isEmpty() && !state.isAfterSearch -> { EmptyTokensList(modifier) } else -> { @@ -313,7 +313,7 @@ private fun TokenItem( .fillMaxWidth() .height(TangemTheme.dimens.size72) .clickable( - enabled = token.available, + enabled = token.isAvailable, onClick = onTokenClick, ) .padding( @@ -336,7 +336,7 @@ private fun TokenItem( EllipsisText( text = token.name, style = TangemTheme.typography.subtitle1, - color = if (token.available) { + color = if (token.isAvailable) { TangemTheme.colors.text.primary1 } else { TangemTheme.colors.text.tertiary @@ -366,7 +366,7 @@ private fun TokenItem( maxLines = 1, softWrap = false, overflow = TextOverflow.Visible, - color = if (token.available) { + color = if (token.isAvailable) { TangemTheme.colors.text.primary1 } else { TangemTheme.colors.text.tertiary @@ -417,7 +417,7 @@ private fun TokenScreenPreview() { availableTokens = listOf(title, token, token, token).toImmutableList(), unavailableTokens = listOf(title, token, token, token).toImmutableList(), tokensListData = TokenListUMData.EmptyList, - afterSearch = false, + isAfterSearch = false, isBalanceHidden = false, onSearchEntered = {}, onTokenSelected = {}, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index 97e9c3dd4d..2d51569af0 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -48,7 +48,7 @@ fun SwapSuccessScreen(state: SwapSuccessStateHolder, onBack: () -> Unit) { SwapSuccessScreenButtons( textRes = R.string.common_close, txUrl = state.txUrl, - showStatusButton = state.showStatusButton, + shouldShowStatusButton = state.shouldShowStatusButton, onExploreClick = state.onExploreButtonClick, onStatusClick = state.onStatusButtonClick, onDoneClick = onBack, @@ -160,7 +160,7 @@ private fun SwapAmountBlock( private fun SwapSuccessScreenButtons( @StringRes textRes: Int, txUrl: String, - showStatusButton: Boolean, + shouldShowStatusButton: Boolean, onExploreClick: () -> Unit, onStatusClick: () -> Unit, onDoneClick: () -> Unit, @@ -178,7 +178,7 @@ private fun SwapSuccessScreenButtons( onClick = onExploreClick, modifier = Modifier.weight(1f), ) - if (showStatusButton) { + if (shouldShowStatusButton) { SpacerW12() SecondaryButtonIconStart( text = stringResourceSafe(id = R.string.express_cex_status_button_title), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index a529440cd6..f344f04903 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -46,7 +46,6 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.SwapTokenScreenTestTags import com.tangem.core.ui.utils.ImageBackgroundContrastChecker -import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.TransactionCardType @@ -217,9 +216,9 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie } SpacerW16() if (balance.isNotBlank()) { - AnimatedContent(targetState = balance, label = "") { + AnimatedContent(targetState = balance, label = "") { balanceText -> Text( - text = it, + text = balanceText, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, modifier = Modifier @@ -318,15 +317,15 @@ private fun Content( style = TangemTheme.typography.body2, ) } else { - AnimatedContent(targetState = amountEquivalent, label = "") { + AnimatedContent(targetState = amountEquivalent, label = "") { amount -> Text( - text = it, + text = amount, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, ) } } - if (type.showWarning) { + if (type.shouldShowWarning) { SpacerW4() IconButton( onClick = { @@ -348,9 +347,9 @@ private fun Content( } } } else { - AnimatedContent(targetState = amountEquivalent, label = "") { + AnimatedContent(targetState = amountEquivalent, label = "") { amount -> Text( - text = it, + text = amount, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20), @@ -409,6 +408,7 @@ fun Token( } } +@Suppress("NullableToStringCall") @Composable private fun TokenIcon( tokenIconUrl: String, @@ -595,7 +595,7 @@ private fun TransactionCardPreview() { private fun TransactionCardPreviewWithPriceImpact() { TransactionCard( type = TransactionCardType.ReadOnly( - showWarning = true, + shouldShowWarning = true, accountTitleUM = AccountTitleUM.Account( prefixText = resourceReference(R.string.common_from), name = AccountNameUM.DefaultMain.value, 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/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt b/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt index 408d809672..df83da98da 100644 --- a/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt +++ b/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt @@ -8,16 +8,19 @@ interface TangemPayOnboardingComponent : ComposableContentComponent { sealed class Params { - abstract val userWalletId: UserWalletId? - data class Deeplink( val deeplink: String, - override val userWalletId: UserWalletId?, ) : Params() data class ContinueOnboarding( - override val userWalletId: UserWalletId?, + val userWalletId: UserWalletId, ) : Params() + + data class FromBannerOnMain( + val userWalletId: UserWalletId, + ) : Params() + + data object FromBannerInSettings : Params() } interface Factory : ComponentFactory diff --git a/features/tangempay/onboarding/impl/build.gradle.kts b/features/tangempay/onboarding/impl/build.gradle.kts index 7b71ee3d94..c94b59ab29 100644 --- a/features/tangempay/onboarding/impl/build.gradle.kts +++ b/features/tangempay/onboarding/impl/build.gradle.kts @@ -27,6 +27,8 @@ dependencies { implementation(projects.features.tangempay.onboarding.api) implementation(projects.features.tangempay.details.api) implementation(projects.features.kyc.api) + implementation(projects.features.wallet.api) + implementation(projects.features.hotWallet.api) /** Domain */ implementation(projects.domain.visa) @@ -40,6 +42,7 @@ dependencies { implementation(deps.compose.material3) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) + implementation(deps.decompose.ext.compose) /** DI */ implementation(deps.hilt.android) @@ -48,4 +51,5 @@ dependencies { /** Other */ implementation(deps.timber) implementation(deps.arrow.core) + implementation(deps.kotlin.immutable.collections) } \ No newline at end of file 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/components/DefaultTangemPayOnboardingComponent.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt index 4ee5eb136c..e11789c134 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayOnboardingComponent.kt @@ -4,9 +4,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.ComponentContext import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.features.tangempay.model.TangemPayOnboardingModel +import com.tangem.features.tangempay.ui.TangemPayOnboardingNavigation import com.tangem.features.tangempay.ui.TandemPayOnboardingScreen import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -19,10 +25,32 @@ internal class DefaultTangemPayOnboardingComponent @AssistedInject constructor( private val model: TangemPayOnboardingModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = TangemPayOnboardingNavigation.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() TandemPayOnboardingScreen(modifier = modifier, state = state) + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + navigation: TangemPayOnboardingNavigation, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent { + val context = childByContext(componentContext) + return when (navigation) { + is TangemPayOnboardingNavigation.WalletSelector -> TangemPayWalletSelectorComponent( + appComponentContext = context, + params = TangemPayWalletSelectorComponent.Params(listener = model, walletsIds = navigation.walletsIds), + ) + } } @AssistedFactory diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayWalletSelectorComponent.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayWalletSelectorComponent.kt new file mode 100644 index 0000000000..d8e84fe4e0 --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayWalletSelectorComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.tangempay.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.ComposableBottomSheetComponent +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.tangempay.model.TangemPayWalletSelectorModel +import com.tangem.features.tangempay.ui.WalletSelectorBottomSheet + +internal class TangemPayWalletSelectorComponent( + appComponentContext: AppComponentContext, + params: Params, +) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext { + + private val model: TangemPayWalletSelectorModel = getOrCreateModel(params = params) + + override fun dismiss() { + model.onDismiss() + } + + @Composable + override fun BottomSheet() { + val state by model.uiState.collectAsStateWithLifecycle() + WalletSelectorBottomSheet(state = state) + } + + data class Params( + val listener: WalletSelectorListener, + val walletsIds: List, + ) +} + +internal interface WalletSelectorListener { + fun onWalletSelected(userWalletId: UserWalletId) + fun onWalletSelectorDismiss() +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt index cb6bf2f491..8c9400fe8e 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt @@ -3,7 +3,6 @@ package com.tangem.features.tangempay.deeplink import android.net.Uri import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.features.tangempay.TangemPayFeatureToggles import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -13,15 +12,12 @@ internal class DefaultOnboardVisaDeepLinkHandler @AssistedInject constructor( @Assisted uri: Uri, appRouter: AppRouter, tangemPayFeatureToggles: TangemPayFeatureToggles, - getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, ) : OnboardVisaDeepLinkHandler { init { if (tangemPayFeatureToggles.isTangemPayEnabled) { - val userWallet = getSelectedWalletSyncUseCase.invoke().getOrNull() val mode = AppRoute.TangemPayOnboarding.Mode.Deeplink( deeplink = uri.toString(), - userWalletId = userWallet?.walletId, ) appRouter.push(AppRoute.TangemPayOnboarding(mode)) } else { diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingModelsModule.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingModelsModule.kt index b4b1892e56..3cd13c821b 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingModelsModule.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayOnboardingModelsModule.kt @@ -3,6 +3,7 @@ package com.tangem.features.tangempay.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model import com.tangem.features.tangempay.model.TangemPayOnboardingModel +import com.tangem.features.tangempay.model.TangemPayWalletSelectorModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -16,5 +17,10 @@ internal interface TangemPayOnboardingModelsModule { @Binds @IntoMap @ClassKey(TangemPayOnboardingModel::class) - fun bindModel(model: TangemPayOnboardingModel): Model + fun bindTangemPayOnboardingModel(model: TangemPayOnboardingModel): Model + + @Binds + @IntoMap + @ClassKey(TangemPayWalletSelectorModel::class) + fun bindTangemPayWalletSelectorModel(model: TangemPayWalletSelectorModel): Model } \ No newline at end of file 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..bc39e6d47b 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 @@ -1,6 +1,9 @@ package com.tangem.features.tangempay.model import androidx.compose.runtime.Stable +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped @@ -8,17 +11,16 @@ 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.navigation.url.UrlOpener -import com.tangem.data.pay.util.TangemPayWalletsManager import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.tangempay.TangemPayAnalyticsEvents -import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.tangempay.TangemPayConstants import com.tangem.features.tangempay.components.TangemPayOnboardingComponent +import com.tangem.features.tangempay.components.WalletSelectorListener import com.tangem.features.tangempay.model.transformers.TangemPayOnboardingButtonLoadingTransformer +import com.tangem.features.tangempay.ui.TangemPayOnboardingNavigation import com.tangem.features.tangempay.ui.TangemPayOnboardingScreenState import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow @@ -40,36 +42,41 @@ internal class TangemPayOnboardingModel @Inject constructor( private val repository: OnboardingRepository, private val produceInitialDataUseCase: ProduceTangemPayInitialDataUseCase, private val urlOpener: UrlOpener, - private val tangemPayWalletsManager: TangemPayWalletsManager, - private val getUserWalletUseCase: GetUserWalletUseCase, - private val getSelectedWalletUseCase: GetSelectedWalletUseCase, -) : Model() { + private val eligibilityManager: TangemPayEligibilityManager, +) : Model(), WalletSelectorListener { private val params = paramsContainer.require() + + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val uiState: StateFlow field = MutableStateFlow(getInitialState()) init { + analytics.send(TangemPayAnalyticsEvents.ActivationScreenOpened()) + init() + } + + private fun init() { modelScope.launch { when (params) { - is TangemPayOnboardingComponent.Params.ContinueOnboarding -> { - checkCustomerInfo() - } is TangemPayOnboardingComponent.Params.Deeplink -> { repository.validateDeeplink(params.deeplink) .onRight { isValid -> if (isValid) showOnboarding() else back() } .onLeft { back() } } + is TangemPayOnboardingComponent.Params.ContinueOnboarding, + is TangemPayOnboardingComponent.Params.FromBannerInSettings, + is TangemPayOnboardingComponent.Params.FromBannerOnMain, + -> showOnboarding() } } } 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, @@ -79,42 +86,25 @@ internal class TangemPayOnboardingModel @Inject constructor( } } - private suspend fun checkCustomerInfo() { - // TODO implement selector - val userWalletId = getUserWalletForPay(params.userWalletId) - repository.getCustomerInfo( - userWalletId = userWalletId, - ) - // selector - .onRight { customerInfo -> - when { - !customerInfo.isKycApproved -> { - when (params) { - is TangemPayOnboardingComponent.Params.Deeplink -> showOnboarding() - else -> openKyc() + private fun checkCustomerInfo(userWalletId: UserWalletId) { + modelScope.launch { + uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true)) + repository.getCustomerInfo( + userWalletId = userWalletId, + ) + .onRight { customerInfo -> + uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) + when { + !customerInfo.isKycApproved -> { + when (params) { + is TangemPayOnboardingComponent.Params.ContinueOnboarding -> openKyc(userWalletId) + else -> startOnboarding(userWalletId) + } } + else -> back() } - else -> back() } - } - .onLeft { back() } - } - - private fun getUserWalletForPay(userWalletId: UserWalletId?): UserWalletId { - val userWallet = userWalletId?.let { getUserWalletUseCase(it).getOrNull() } - return if (userWallet?.isMultiCurrency == true) { - userWallet.walletId - } else { - tryGetSelectedWalletId() - } - } - - private fun tryGetSelectedWalletId(): UserWalletId { - val selectedWallet = getSelectedWalletUseCase.sync().getOrNull() - return if (selectedWallet?.isMultiCurrency == true) { - selectedWallet.walletId - } else { - tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking().walletId + .onLeft { startOnboarding(userWalletId) } } } @@ -125,43 +115,72 @@ internal class TangemPayOnboardingModel @Inject constructor( private fun onGetCardClick() { analytics.send(TangemPayAnalyticsEvents.GetCardClicked()) + modelScope.launch { + val eligibleWalletsIds = eligibilityManager.getEligibleWallets().map { it.walletId } + if (eligibleWalletsIds.isEmpty()) { + back() + return@launch + } + when (params) { + is TangemPayOnboardingComponent.Params.ContinueOnboarding -> { + checkCustomerInfo(params.userWalletId) + } + is TangemPayOnboardingComponent.Params.FromBannerOnMain -> { + if (eligibleWalletsIds.any { walletId -> walletId == params.userWalletId }) { + checkCustomerInfo(userWalletId = params.userWalletId) + } else { + openWalletSelectorIfNeeds(eligibleWalletsIds) + } + } + is TangemPayOnboardingComponent.Params.Deeplink, + is TangemPayOnboardingComponent.Params.FromBannerInSettings, + -> { + openWalletSelectorIfNeeds(eligibleWalletsIds) + } + } + } + } + + private fun openWalletSelectorIfNeeds(eligibleWalletsIds: List) { + if (eligibleWalletsIds.size == 1) { + checkCustomerInfo(userWalletId = eligibleWalletsIds[0]) + } else { + bottomSheetNavigation.activate(TangemPayOnboardingNavigation.WalletSelector(eligibleWalletsIds)) + } + } + + private fun startOnboarding(userWalletId: UserWalletId) { uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true)) modelScope.launch { - // TODO implement selector - val userWalletId = getUserWalletForPay(params.userWalletId) val result = produceInitialDataUseCase(userWalletId) if (result.isLeft()) { - Timber.e("Error producing initial data: ${result.leftOrNull()?.message}") + val errorMessage = result.leftOrNull()?.message ?: "Unknown error" + Timber.e("Error producing initial data: $errorMessage") uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) return@launch } - - // TODO implement selector 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 -> if (customerInfo.isKycApproved) { back() } else { - openKyc() + openKyc(userWalletId) } }, ) } } - private fun openKyc() { - // TODO implement selector + private fun openKyc(userWalletId: UserWalletId) { router.replaceAll( AppRoute.Wallet, - AppRoute.Kyc( - userWalletId = getUserWalletForPay(params.userWalletId), - ), + AppRoute.Kyc(userWalletId = userWalletId), ) } @@ -172,4 +191,13 @@ internal class TangemPayOnboardingModel @Inject constructor( private fun getInitialState(): TangemPayOnboardingScreenState { return TangemPayOnboardingScreenState.Loading(onBack = ::back) } + + override fun onWalletSelected(userWalletId: UserWalletId) { + bottomSheetNavigation.dismiss() + checkCustomerInfo(userWalletId) + } + + override fun onWalletSelectorDismiss() { + bottomSheetNavigation.dismiss() + } } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayWalletSelectorModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayWalletSelectorModel.kt new file mode 100644 index 0000000000..27a15aeeeb --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayWalletSelectorModel.kt @@ -0,0 +1,73 @@ +package com.tangem.features.tangempay.model + +import androidx.compose.runtime.Stable +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.ui.UiMessageSender +import com.tangem.features.tangempay.components.TangemPayWalletSelectorComponent +import com.tangem.features.tangempay.ui.WalletSelectorBSContentUM +import com.tangem.features.wallet.utils.UserWalletsFetcher +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TangemPayWalletSelectorModel @Inject constructor( + paramsContainer: ParamsContainer, + userWalletsFetcherFactory: UserWalletsFetcher.Factory, + messageSender: UiMessageSender, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params = paramsContainer.require() + + private val userWalletsFetcher = userWalletsFetcherFactory.create( + messageSender = messageSender, + onlyMultiCurrency = true, + isAuthMode = false, + isClickableIfLocked = false, + onWalletClick = { params.listener.onWalletSelected(it) }, + ) + + init { + fetchUserWalletsUM() + } + + val uiState: StateFlow + field = MutableStateFlow(getInitialState()) + + private fun getInitialState(): WalletSelectorBSContentUM { + return WalletSelectorBSContentUM( + userWallets = persistentListOf(), + onDismiss = { params.listener.onWalletSelectorDismiss() }, + ) + } + + private fun fetchUserWalletsUM() { + modelScope.launch { + val eligibleWalletsIds = params.walletsIds.map { it.stringValue }.toSet() + userWalletsFetcher.userWallets.collectLatest { userWalletsListUM -> + uiState.update { + WalletSelectorBSContentUM( + userWallets = userWalletsListUM + .filter { wallet -> wallet.id in eligibleWalletsIds } + .toImmutableList(), + onDismiss = { params.listener.onWalletSelectorDismiss() }, + ) + } + } + } + } + + fun onDismiss() { + params.listener.onWalletSelectorDismiss() + } +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingNavigation.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingNavigation.kt new file mode 100644 index 0000000000..8fe01286d7 --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayOnboardingNavigation.kt @@ -0,0 +1,13 @@ +package com.tangem.features.tangempay.ui + +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.serialization.Serializable + +@Serializable +internal sealed class TangemPayOnboardingNavigation { + + @Serializable + data class WalletSelector( + val walletsIds: List, + ) : TangemPayOnboardingNavigation() +} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/WalletSelectorBSContentUM.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/WalletSelectorBSContentUM.kt new file mode 100644 index 0000000000..e0028184e6 --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/WalletSelectorBSContentUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.tangempay.ui + +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import kotlinx.collections.immutable.ImmutableList + +internal data class WalletSelectorBSContentUM( + val userWallets: ImmutableList, + val onDismiss: () -> Unit, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/WalletSelectorBottomSheet.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/WalletSelectorBottomSheet.kt new file mode 100644 index 0000000000..fd3f9a755e --- /dev/null +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/ui/WalletSelectorBottomSheet.kt @@ -0,0 +1,124 @@ +package com.tangem.features.tangempay.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.common.ui.userwallet.UserWalletItem +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.tangempay.onboarding.impl.R +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun WalletSelectorBottomSheet(state: WalletSelectorBSContentUM) { + TangemBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = state.onDismiss, + content = TangemBottomSheetConfigContent.Empty, + ), + onBack = state.onDismiss, + containerColor = TangemTheme.colors.background.tertiary, + title = { content -> + TangemTopAppBar( + title = resourceReference(R.string.common_choose_wallet), + titleAlignment = Alignment.CenterHorizontally, + endButton = TopAppBarButtonUM.Close(onCloseClick = state.onDismiss), + ) + }, + ) { content -> + Content( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing8, + ), + state = state, + ) + } +} + +@Composable +private fun Content(state: WalletSelectorBSContentUM, modifier: Modifier = Modifier) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + Column( + modifier = modifier + .verticalScroll(rememberScrollState()), + ) { + BlockCard( + modifier = Modifier.fillMaxSize(), + colors = TangemBlockCardColors.copy( + containerColor = TangemTheme.colors.background.action, + disabledContainerColor = TangemTheme.colors.background.action, + ), + ) { + state.userWallets.forEach { state -> + key(state.id) { + UserWalletItem( + modifier = Modifier.fillMaxWidth(), + blockColors = TangemBlockCardColors.copy( + containerColor = TangemTheme.colors.background.action, + disabledContainerColor = TangemTheme.colors.background.action, + ), + state = state, + ) + } + } + } + SpacerH(bottomBarHeight) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewWalletSelectorBottomSheet() { + TangemThemePreview { + WalletSelectorBottomSheet( + state = WalletSelectorBSContentUM( + userWallets = persistentListOf( + UserWalletItemUM( + id = "1", + name = stringReference("Wallet 1"), + information = UserWalletItemUM.Information.Loaded(TextReference.Str("3 cards")), + balance = UserWalletItemUM.Balance.Loading, + isEnabled = true, + endIcon = UserWalletItemUM.EndIcon.None, + onClick = {}, + ), + UserWalletItemUM( + id = "2", + name = stringReference("Wallet 2"), + information = UserWalletItemUM.Information.Loaded(TextReference.Str("3 cards")), + balance = UserWalletItemUM.Balance.Loading, + isEnabled = true, + endIcon = UserWalletItemUM.EndIcon.None, + onClick = {}, + ), + ), + onDismiss = {}, + ), + ) + } +} \ No newline at end of file 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 908fdf6c7e..44384391ba 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 7949e74246..5d647616a7 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -126,6 +126,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 43aac5ae23..b136451f60 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,18 +7,17 @@ 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.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isImported -import com.tangem.domain.models.wallet.isLocked -import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.models.wallet.* import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase import com.tangem.domain.notifications.repository.NotificationsRepository @@ -43,6 +42,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 @@ -100,6 +100,10 @@ internal class WalletModel @Inject constructor( private val accountsFeatureToggles: AccountsFeatureToggles, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, private val getAppThemeModeUseCase: GetAppThemeModeUseCase, + private val trackingContextProxy: TrackingContextProxy, + private val singleAccountListSupplier: SingleAccountListSupplier, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val feedFeatureToggle: FeedFeatureToggle, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -117,13 +121,7 @@ internal class WalletModel @Inject constructor( private var expressTxStatusTaskScheduler = SingleTaskScheduler() init { - screenLifecycleProvider.isBackgroundState - .onEach { isBackground -> - if (isBackground.not()) { - suggestToEnableBiometrics() - } - }.launchIn(modelScope) - + updateMarketToggle() suggestToOpenMarkets() maybeMigrateNames() @@ -140,6 +138,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) { @@ -195,7 +205,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 { @@ -278,14 +287,39 @@ internal class WalletModel @Inject constructor( .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 + } + val result = getAppThemeModeUseCase().firstOrNull() + val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM + analyticsEventsHandler.send( + WalletScreenAnalyticsEvent.MainScreen.ScreenOpened( + hasMobileWallet = hasMobileWallet, + accountsCount = accountsCount, + theme = theme.value, + isImported = selectedWallet.isImported(), + ), + ) + } + } + if (selectedWallet.isMultiCurrency) { selectedWalletAnalyticsSender.send(selectedWallet) } subscribeOnExpressTransactionsUpdates(selectedWallet) observeAndClearNFTCacheIfNeedUseCase(selectedWallet) - - sendMainScreenOpenedAnalytics(selectedWallet.isImported()) } .flowOn(dispatchers.main) .launchIn(modelScope) @@ -419,7 +453,8 @@ internal class WalletModel @Inject constructor( private suspend fun updateWallets(action: WalletsUpdateActionResolver.Action) { when (action) { is WalletsUpdateActionResolver.Action.InitializeWallets -> initializeWallets(action) - is WalletsUpdateActionResolver.Action.ReinitializeWallet -> reinitializeWallet(action) + is WalletsUpdateActionResolver.Action.ReinitializeNewWallet -> reinitializeNewWallet(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) @@ -502,7 +537,7 @@ internal class WalletModel @Inject constructor( } } - private fun reinitializeWallet(action: WalletsUpdateActionResolver.Action.ReinitializeWallet) { + private fun reinitializeNewWallet(action: WalletsUpdateActionResolver.Action.ReinitializeNewWallet) { walletScreenContentLoader.cancel(action.prevWalletId) tokenListStore.remove(action.prevWalletId) @@ -515,7 +550,7 @@ internal class WalletModel @Inject constructor( fetchWalletContent(userWallet = action.selectedWallet) stateHolder.update( - ReinitializeWalletTransformer( + ReinitializeNewWalletTransformer( prevWalletId = action.prevWalletId, newUserWallet = action.selectedWallet, clickIntents = clickIntents, @@ -524,6 +559,29 @@ 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, + ) + + fetchWalletContent(userWallet = userWallet) + + stateHolder.update( + ReinitializeWalletTransformer( + userWallet = userWallet, + clickIntents = clickIntents, + walletImageResolver = walletImageResolver, + ), + ) + } + } + private fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) { if (accountsFeatureToggles.isFeatureEnabled) { fetchWalletContent(userWallet = action.selectedWallet) @@ -695,17 +753,6 @@ internal class WalletModel @Inject constructor( } } - private suspend fun sendMainScreenOpenedAnalytics(isImported: Boolean) { - val result = getAppThemeModeUseCase().firstOrNull() - val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM - analyticsEventsHandler.send( - WalletScreenAnalyticsEvent.MainScreen.ScreenOpened( - theme = theme.value, - isImported = isImported, - ), - ) - } - inner class AskBiometryModelCallbacks : AskBiometryComponent.ModelCallbacks { override fun onAllowed() { analyticsEventsHandler.send(MainScreenAnalyticsEvent.EnableBiometrics(AnalyticsParam.OnOffState.On)) 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..d498e480a0 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,14 +64,17 @@ 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) } isAnotherWalletSelected(state, selectedWallet) -> { - Action.ReinitializeWallet( + Action.ReinitializeNewWallet( prevWalletId = state.getPrevSelectedWallet().id, selectedWallet = 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 } @@ -296,7 +291,7 @@ internal class WalletsUpdateActionResolver @Inject constructor( * @property prevWalletId previous selected wallet id * @property selectedWallet selected wallet */ - data class ReinitializeWallet( + data class ReinitializeNewWallet( val prevWalletId: UserWalletId, val selectedWallet: UserWallet, ) : Action() { @@ -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/TangemPayClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt index ed0005c791..b00b4b5886 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.child.wallet.model.intents +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.res.R @@ -14,6 +15,7 @@ import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHideOnboardingStateTransformer import com.tangem.features.tangempay.TangemPayFeatureToggles import kotlinx.coroutines.launch import javax.inject.Inject @@ -29,6 +31,10 @@ internal interface TangemPayIntents { fun onIssuingFailedClicked() fun onPaySupportClick() + + fun onOnboardingBannerClick(userWalletId: UserWalletId) + + fun onOnboardingBannerCloseClick(userWalletId: UserWalletId) } @Suppress("LongParameterList") @@ -115,4 +121,15 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( ) } } + + override fun onOnboardingBannerClick(userWalletId: UserWalletId) { + router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain(userWalletId)) + } + + override fun onOnboardingBannerCloseClick(userWalletId: UserWalletId) { + modelScope.launch { + stateHolder.update(transformer = TangemPayHideOnboardingStateTransformer(userWalletId)) + onboardingRepository.setHideMainOnboardingBanner(userWalletId) + } + } } \ No newline at end of file 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..8d1c6bd283 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,8 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( error.handle( onAlreadyUnlocked = {}, onUserCancelled = {}, + analyticsEventHandler = analyticsEventHandler, + isFromUnlockAll = true, showMessage = uiMessageSender::send, ) } @@ -221,7 +220,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 +248,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 +306,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 +335,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 +343,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 +357,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 +365,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 +375,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 +393,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 +402,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 +425,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 +444,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 +467,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 +507,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 +546,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/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 98d68adf5f..934d258f18 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -111,14 +111,8 @@ internal class DefaultWalletRouter @Inject constructor( ) } - override fun openTangemPayOnboarding(userWalletId: UserWalletId) { - router.push( - AppRoute.TangemPayOnboarding( - AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding( - userWalletId = userWalletId, - ), - ), - ) + override fun openTangemPayOnboarding(mode: AppRoute.TangemPayOnboarding.Mode) { + router.push(route = AppRoute.TangemPayOnboarding(mode = mode)) } override fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index cf9d3ea617..c423b80481 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Stable import com.arkivanov.decompose.router.slot.SlotNavigation +import com.tangem.common.routing.AppRoute import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -63,7 +64,7 @@ internal interface InnerWalletRouter { fun openTokenReceiveBottomSheet(tokenReceiveConfig: TokenReceiveConfig) - fun openTangemPayOnboarding(userWalletId: UserWalletId) + fun openTangemPayOnboarding(mode: AppRoute.TangemPayOnboarding.Mode) fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) 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 3ecfa644f8..dec57f0b23 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,15 +51,44 @@ sealed class WalletScreenAnalyticsEvent { params: Map = mapOf(), ) : AnalyticsEvent(category = "Main Screen", event = event, params = params) { - class ScreenOpened( + data class ScreenOpened( + private val hasMobileWallet: Boolean, + private val accountsCount: Int?, val theme: String, val isImported: Boolean, ) : MainScreen( event = "Screen opened", + params = buildMap { + put("Mobile Wallet", if (hasMobileWallet) "Yes" else "No") + if (accountsCount != null) put("Accounts Count", accountsCount.toString()) + put("App Theme", theme) + val seedPhrase = if (isImported) { + "Seed Phrase" + } else { + "Seedless" + } + put("Wallet Type", seedPhrase) + }, + ) + + data class NoticeFinishActivation( + private val activationState: ActivationState, + private val balanceState: AnalyticsParam.EmptyFull, + ) : MainScreen( + event = "Notice - Finish Activation", params = mapOf( - "App Theme" to theme, - "Wallet Type" to if (isImported) "Seed Phrase" else "Seedless", + "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( @@ -79,9 +108,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, @@ -90,54 +119,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 } @@ -146,8 +175,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 ccb889e197..f637e0bfc4 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 @@ -29,7 +29,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..69b492660a 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,18 +1,38 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils +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 +import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @ModelScoped 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: ConcurrentHashMap = ConcurrentHashMap() suspend fun send( userWalletId: UserWalletId, @@ -25,10 +45,60 @@ internal class WalletWarningsSingleEventSender @Inject constructor( val events = newWarnings.filter { it !in displayedUiState.warnings } + // We must show activation bs only for the first seen wallet when open the app (if need, see conditions below), + // so we keep this wallet id and use for future checks, ignore other wallets during the app session. + if (isActivationBottomSheetShown.isEmpty()) { + isActivationBottomSheetShown[userWalletId] = false + } + 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 -> { + // We check that map contains the first seen wallet (will return null instead false/true otherwise) + // and for this wallet we haven't shown the activation bs yet (check that returns false, not true) + if (isActivationBottomSheetShown[userWalletId] == false) { + if (event.type == WalletActivationBannerType.Warning) { + showFinishActivationBottomSheet(userWalletId) + } + isActivationBottomSheetShown[userWalletId] = 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/TangemPayState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt index bbe8472d49..4eae554a24 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt @@ -11,6 +11,11 @@ internal sealed class TangemPayState { data object Loading : TangemPayState() + data class OnboardingBanner( + val onClick: () -> Unit, + val closeOnClick: () -> Unit, + ) : TangemPayState() + data class Progress( val title: TextReference, val description: TextReference, @@ -39,4 +44,6 @@ internal sealed class TangemPayState { ) : TangemPayState() data class TemporaryUnavailable(val notification: WalletNotification) : TangemPayState() + + data object ExposedDevice : TangemPayState() } \ 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/ReinitializeNewWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt new file mode 100644 index 0000000000..d6072f30f7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt @@ -0,0 +1,46 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory +import kotlinx.collections.immutable.toImmutableList + +/** + * Reinitialize and place new wallet at the end transformer + * + * @property prevWalletId reinitialized wallet id + * @property newUserWallet new user wallet + * @property clickIntents click intents + * +[REDACTED_AUTHOR] + */ +internal class ReinitializeNewWalletTransformer( + private val prevWalletId: UserWalletId, + private val newUserWallet: UserWallet, + private val clickIntents: WalletClickIntents, + private val walletImageResolver: WalletImageResolver, +) : WalletScreenStateTransformer { + + private val walletLoadingStateFactory by lazy { + WalletLoadingStateFactory( + clickIntents = clickIntents, + walletImageResolver = walletImageResolver, + ) + } + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + wallets = prevState.wallets + .filterNot { it.walletCardState.id == prevWalletId } + .plus( + element = walletLoadingStateFactory.create( + userWallet = newUserWallet, + ), + ) + .toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt index 4f4fa993ca..4459476877 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt @@ -1,28 +1,23 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory -import kotlinx.collections.immutable.toImmutableList /** * Reinitialize wallet transformer * - * @property prevWalletId reinitialized wallet id - * @property newUserWallet new user wallet - * @property clickIntents click intents - * -[REDACTED_AUTHOR] + * @property userWallet user wallet to reinitialize + * @property clickIntents click intents + * @property walletImageResolver wallet image resolver */ internal class ReinitializeWalletTransformer( - private val prevWalletId: UserWalletId, - private val newUserWallet: UserWallet, + private val userWallet: UserWallet, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, -) : WalletScreenStateTransformer { +) : WalletStateTransformer(userWalletId = userWallet.walletId) { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( @@ -31,16 +26,9 @@ internal class ReinitializeWalletTransformer( ) } - override fun transform(prevState: WalletScreenState): WalletScreenState { - return prevState.copy( - wallets = prevState.wallets - .filterNot { it.walletCardState.id == prevWalletId } - .plus( - element = walletLoadingStateFactory.create( - userWallet = newUserWallet, - ), - ) - .toImmutableList(), + override fun transform(prevState: WalletState): WalletState { + return walletLoadingStateFactory.create( + userWallet = userWallet, ) } } \ 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/TangemPayExposedDeviceTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayExposedDeviceTransformer.kt new file mode 100644 index 0000000000..b8a735a4f4 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayExposedDeviceTransformer.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState + +internal class TangemPayExposedDeviceTransformer( + userWalletId: UserWalletId, +) : WalletStateTransformer(userWalletId) { + override fun transform(prevState: WalletState): WalletState { + return if (prevState is WalletState.MultiCurrency.Content) { + prevState.copy(tangemPayState = TangemPayState.ExposedDevice) + } else { + prevState + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt new file mode 100644 index 0000000000..9e4e6bafd7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState + +internal class TangemPayHideOnboardingStateTransformer( + userWalletId: UserWalletId, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return if (prevState is WalletState.MultiCurrency.Content) { + prevState.copy(tangemPayState = TangemPayState.Empty) + } else { + prevState + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt new file mode 100644 index 0000000000..15b2ad4dee --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt @@ -0,0 +1,25 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState + +internal class TangemPayOnboardingBannerStateTransformer( + userWalletId: UserWalletId, + private val onClick: (UserWalletId) -> Unit, + private val closeOnClick: (UserWalletId) -> Unit, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return if (prevState is WalletState.MultiCurrency.Content) { + prevState.copy( + tangemPayState = TangemPayState.OnboardingBanner( + onClick = { onClick(userWalletId) }, + closeOnClick = { closeOnClick(userWalletId) }, + ), + ) + } else { + prevState + } + } +} \ No newline at end of file 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/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt index c6ef0d9981..ad35b058ea 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -42,6 +42,7 @@ internal class MultiWalletCurrencyActionsConverter( } } + @Suppress("LongMethod") private fun mapTokenActionState( actionsState: TokenActionsState.ActionState, cryptoCurrencyStatus: CryptoCurrencyStatus, @@ -105,6 +106,11 @@ internal class MultiWalletCurrencyActionsConverter( icon = R.drawable.ic_analytics_24 action = { clickIntents.onAnalyticsClick(cryptoCurrencyStatus) } } + is TokenActionsState.ActionState.YieldMode -> { + title = resourceReference(R.string.yield_module_start_earning) + icon = R.drawable.ic_analytics_up_mini_24 + action = { /* no-op */ } + } } return TokenActionButtonConfig( 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 fa02716faf..f4b38a9290 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 @@ -52,7 +52,6 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { stakingAvailabilityMap: Map = emptyMap(), shouldShowMainPromo: Boolean = false, ) { - val accountFlattenCurrencies = accountList.flattenCurrencies() val mainAccount = accountList.mainAccount when { @@ -74,26 +73,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, - stakingAvailabilityMap = stakingAvailabilityMap, - shouldShowMainPromo = shouldShowMainPromo, - ) - } + val convertParams = TokenConverterParams.Account(accountList, expandedAccounts) + updateContent( + params = convertParams, + appCurrency = appCurrency, + yieldSupplyApyMap = yieldSupplyApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, + shouldShowMainPromo = shouldShowMainPromo, + ) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt index 6f182566d8..6a7cf0a950 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers +import com.tangem.common.routing.AppRoute import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.model.MainCustomerInfoContentState @@ -57,7 +58,10 @@ internal class TangemPayMainSubscriber @AssistedInject constructor( transformer = TangemPayUnavailableStateTransformer(userWalletId), ) } - else -> { + TangemPayCustomerInfoError.ExposedDeviceError -> { + stateController.update(TangemPayExposedDeviceTransformer(userWalletId)) + } + TangemPayCustomerInfoError.UnknownError -> { // hide TangemPay block Timber.e("Failed when loading main screen TangemPay info: $tangemPayError") stateController.update( @@ -79,6 +83,13 @@ internal class TangemPayMainSubscriber @AssistedInject constructor( updateTangemPay(data = state.info, userWalletId = userWalletId) analytics.send(customerInfo = state.info) } + is MainCustomerInfoContentState.OnboardingBanner -> stateController.update( + transformer = TangemPayOnboardingBannerStateTransformer( + userWalletId = userWalletId, + onClick = clickIntents::onOnboardingBannerClick, + closeOnClick = clickIntents::onOnboardingBannerCloseClick, + ), + ) } } @@ -91,7 +102,11 @@ internal class TangemPayMainSubscriber @AssistedInject constructor( userWalletId = userWalletId, value = data, cardFrozenState = cardFrozenState, - onClickKyc = { innerWalletRouter.openTangemPayOnboarding(userWalletId) }, + onClickKyc = { + innerWalletRouter.openTangemPayOnboarding( + mode = AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding(userWalletId), + ) + }, onIssuingCard = clickIntents::onIssuingCardClicked, onIssuingFailed = clickIntents::onIssuingFailedClicked, openDetails = { config -> 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/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayExposedDeviceState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayExposedDeviceState.kt new file mode 100644 index 0000000000..6ba66da79f --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayExposedDeviceState.kt @@ -0,0 +1,41 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.visa + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import com.tangem.core.ui.R +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.inputrow.InputRowImageBase +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme + +private const val DISABLED_ALPHA = 0.6F + +@Composable +internal fun TangemPayExposedDeviceState(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary) + .alpha(DISABLED_ALPHA), + enabled = false, + onClick = {}, + ) { + InputRowImageBase( + modifier = Modifier + .padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = resourceReference(R.string.tangem_pay_rooted_device_subtitle), + subtitleColor = TangemTheme.colors.text.primary1, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + endIconTint = TangemTheme.colors.icon.warning, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt index 24f11c043c..b8f7e9a587 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt @@ -23,7 +23,9 @@ internal fun TangemPayMainScreenBlock(state: TangemPayState, isBalanceHidden: Bo is TangemPayState.RefreshNeeded -> TangemPayRefreshBlock(state, modifier) is TangemPayState.TemporaryUnavailable -> TangemPayUnavailableBlock(state, modifier) is TangemPayState.FailedIssue -> TangemPayFailedIssueState(state, modifier) - TangemPayState.Loading -> TangemPayLoadingScreenBlock(modifier) + is TangemPayState.OnboardingBanner -> TangemPayOnboardingBanner(state, modifier) + is TangemPayState.ExposedDevice -> TangemPayExposedDeviceState(modifier) + is TangemPayState.Loading -> TangemPayLoadingScreenBlock(modifier) } } @@ -35,6 +37,8 @@ private fun TangemPayMainScreenBlockPreview() { Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { TangemPayMainScreenBlock(state = TangemPayState.Loading, isBalanceHidden = false) + TangemPayMainScreenBlock(state = TangemPayState.ExposedDevice, isBalanceHidden = false) + TangemPayMainScreenBlock( Progress( title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayOnboardingBanner.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayOnboardingBanner.kt new file mode 100644 index 0000000000..b57061cb36 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayOnboardingBanner.kt @@ -0,0 +1,129 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.visa + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +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.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH4 +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState + +private const val GRADIENT_START_COLOR = 0xFF252934 +private const val GRADIENT_END_COLOR = 0xFF12141E +private const val GRADIENT_OFFSET_X = 0f +private const val GRADIENT_OFFSET_Y = 80F +private const val GRADIENT_RADIUS = 200F + +@Composable +internal fun TangemPayOnboardingBanner(state: TangemPayState.OnboardingBanner, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background( + brush = Brush.radialGradient( + colors = listOf(Color(GRADIENT_START_COLOR), Color(GRADIENT_END_COLOR)), + center = Offset(GRADIENT_OFFSET_X, GRADIENT_OFFSET_Y), + radius = GRADIENT_RADIUS, + ), + ) + .clickable(onClick = state.onClick), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(R.drawable.img_tangem_pay_onboarding_banner), + contentDescription = null, + modifier = Modifier + .padding(horizontal = 8.dp) + .width(55.dp) + .height(95.dp) + .offset(y = 10.dp), + ) + + SpacerH4() + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResourceSafe(R.string.tangempay_onboarding_banner_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.constantWhite, + ) + + SpacerH(6.dp) + + Text( + text = stringResourceSafe(R.string.tangempay_onboarding_banner_description), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + Box( + modifier = Modifier.fillMaxHeight(), + contentAlignment = Alignment.TopEnd, + ) { + Image( + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .clickable(onClick = state.closeOnClick) + .padding(4.dp) + .padding(top = 12.dp) + .size(12.dp), + painter = painterResource(id = R.drawable.ic_close_24), + colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive), + contentDescription = null, + ) + } + } + } +} + +@Preview(showBackground = true) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewTangemOnboardingBanner() { + TangemThemePreview { + TangemPayOnboardingBanner( + TangemPayState.OnboardingBanner( + onClick = {}, + closeOnClick = {}, + ), + ) + } +} \ No newline at end of file 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..b5b89566f2 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 { @@ -87,9 +108,10 @@ internal class WelcomeModel @Inject constructor( routedOut = true router.replaceAll(AppRoute.Wallet) } - .onLeft { - it.handle( + .onLeft { error -> + error.handle( specificWalletId = null, + isFromUnlockAll = true, onUserCancelled = { tryToUnlockWithAccessCodeRightAway() }, ) setSelectWalletState() @@ -119,13 +141,18 @@ internal class WelcomeModel @Inject constructor( showUnlockWithBiometricButton = canUnlockWithBiometrics(), addWalletClick = ::addWalletClick, onUnlockWithBiometricClick = { + analyticsEventHandler.send(SignIn.ButtonUnlockAllWithBiometric()) modelScope.launch { userWalletsListRepository.unlockAllWallets() .onRight { router.replaceAll(AppRoute.Wallet) } - .onLeft { - it.handle(null, onUserCancelled = { /* ignore */ }) + .onLeft { error -> + error.handle( + specificWalletId = null, + isFromUnlockAll = true, + onUserCancelled = { /* ignore */ }, + ) } } }, @@ -140,6 +167,7 @@ internal class WelcomeModel @Inject constructor( } private fun addWalletClick() { + analyticsEventHandler.send(SignIn.ButtonAddWallet(AnalyticsParam.ScreensSources.SignIn)) router.push(AppRoute.CreateWalletSelection) } @@ -154,6 +182,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 } @@ -178,11 +207,19 @@ internal class WelcomeModel @Inject constructor( router.replaceAll(AppRoute.Wallet) } .onLeft { error -> - error.handle(specificWalletId = userWalletId, onUserCancelled = { /* ignore*/ }) + error.handle( + specificWalletId = userWalletId, + isFromUnlockAll = false, + onUserCancelled = { /* ignore*/ }, + ) } } - suspend fun UnlockWalletError.handle(specificWalletId: UserWalletId?, onUserCancelled: suspend () -> Unit = { }) { + suspend fun UnlockWalletError.handle( + specificWalletId: UserWalletId?, + isFromUnlockAll: Boolean, + onUserCancelled: suspend () -> Unit = { }, + ) { handle( onAlreadyUnlocked = { // this should not happen, as we check for locked state before this @@ -190,6 +227,8 @@ internal class WelcomeModel @Inject constructor( router.replaceAll(AppRoute.Wallet) }, onUserCancelled = { onUserCancelled() }, + analyticsEventHandler = analyticsEventHandler, + isFromUnlockAll = isFromUnlockAll, showMessage = uiMessageSender::send, ) } @@ -203,4 +242,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 4b7b61f898..6d5f950820 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/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index c6bb211f29..25e0ab888d 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -78,6 +78,7 @@ internal class YieldSupplyModel @Inject constructor( var userWallet: UserWallet by Delegates.notNull() private val fetchCurrencyJobHolder = JobHolder() + private val loadStatusJobHolder = JobHolder() private var lastStatusCheckTimestamp = 0L private val isFirstCryptoCurrencyStatusEmission = AtomicBoolean(true) @@ -134,22 +135,20 @@ internal class YieldSupplyModel @Inject constructor( } } - private fun loadTokenStatus() { + private suspend fun loadTokenStatus() { val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return - modelScope.launch(dispatchers.default) { - yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken) - .onRight { tokenStatus -> - uiState.update( - YieldSupplyTokenStatusSuccessTransformer( - tokenStatus = tokenStatus, - onStartEarningClick = ::onStartEarningClick, - ), - ) - }.onLeft { - Timber.e(it) - uiState.update { YieldSupplyUM.Initial } - } - } + yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken) + .onRight { tokenStatus -> + uiState.update( + YieldSupplyTokenStatusSuccessTransformer( + tokenStatus = tokenStatus, + onStartEarningClick = ::onStartEarningClick, + ), + ) + }.onLeft { + Timber.e(it) + uiState.update { YieldSupplyUM.Initial } + } } override fun onStartEarningClick() { @@ -183,7 +182,9 @@ internal class YieldSupplyModel @Inject constructor( } @Suppress("MaximumLineLength") - private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) = modelScope.launch { + private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) = modelScope.launch( + dispatchers.default, + ) { val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus val tokenProtocolStatus = yieldSupplyRepository.getTokenProtocolStatus( userWallet.walletId, @@ -234,7 +235,7 @@ internal class YieldSupplyModel @Inject constructor( lastStatusCheckTimestamp = 0L } } - } + }.saveIn(loadStatusJobHolder) private fun showProcessing(status: YieldSupplyEnterStatus) { uiState.update { @@ -246,24 +247,21 @@ internal class YieldSupplyModel @Inject constructor( fetchCurrencyWithDelay() } - private fun loadStatus(cryptoCurrencyStatus: CryptoCurrencyStatus) { + private suspend fun loadStatus(cryptoCurrencyStatus: CryptoCurrencyStatus) { val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus - modelScope - .launch { - yieldSupplyRepository.saveTokenProtocolStatus( - userWalletId = userWallet.walletId, - cryptoCurrency = cryptoCurrency, - yieldSupplyEnterStatus = null, - ) - if (yieldSupplyStatus?.isActive == true) { - loadActiveState( - cryptoCurrencyStatus = cryptoCurrencyStatus, - yieldSupplyStatus = yieldSupplyStatus, - ) - } else { - loadTokenStatus() - } - } + yieldSupplyRepository.saveTokenProtocolStatus( + userWalletId = userWallet.walletId, + cryptoCurrency = cryptoCurrency, + yieldSupplyEnterStatus = null, + ) + if (yieldSupplyStatus?.isActive == true) { + loadActiveState( + cryptoCurrencyStatus = cryptoCurrencyStatus, + yieldSupplyStatus = yieldSupplyStatus, + ) + } else { + loadTokenStatus() + } } private fun fetchCurrencyWithDelay() { @@ -280,7 +278,10 @@ internal class YieldSupplyModel @Inject constructor( }.saveIn(fetchCurrencyJobHolder) } - private fun loadActiveState(cryptoCurrencyStatus: CryptoCurrencyStatus, yieldSupplyStatus: YieldSupplyStatus) { + private suspend fun loadActiveState( + cryptoCurrencyStatus: CryptoCurrencyStatus, + yieldSupplyStatus: YieldSupplyStatus, + ) { val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return val showWarningIcon = !yieldSupplyStatus.isAllowedToSpend val isShowInfoIconPrevState = when (val state = uiState.value) { @@ -295,50 +296,48 @@ internal class YieldSupplyModel @Inject constructor( ), ) } - modelScope.launch(dispatchers.default) { - yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken) - .onRight { tokenStatus -> - uiState.update { - YieldSupplyUM.Content( - title = resourceReference( - R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, + yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken) + .onRight { tokenStatus -> + uiState.update { + YieldSupplyUM.Content( + title = resourceReference( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, + ), + subtitle = resourceReference( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle, + ), + rewardsApy = combinedReference( + resourceReference( + R.string.yield_module_token_details_earn_notification_apy, ), - subtitle = resourceReference( - R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle, - ), - rewardsApy = combinedReference( - resourceReference( - R.string.yield_module_token_details_earn_notification_apy, - ), - stringReference(" ${tokenStatus.apy}%"), - ), - onClick = ::onActiveClick, - showWarningIcon = showWarningIcon, - showInfoIcon = isShowInfoIconPrevState, - apy = tokenStatus.apy.toString(), - ) - } - computeAndApplyShowInfoIcon(cryptoCurrencyStatus) - }.onLeft { t -> - Timber.e(t) - uiState.update { - YieldSupplyUM.Content( - title = resourceReference( - R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, - ), - subtitle = resourceReference( - R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle, - ), - rewardsApy = TextReference.EMPTY, - onClick = ::onActiveClick, - showWarningIcon = showWarningIcon, - showInfoIcon = isShowInfoIconPrevState, - apy = "", - ) - } - computeAndApplyShowInfoIcon(cryptoCurrencyStatus) + stringReference(" ${tokenStatus.apy}%"), + ), + onClick = ::onActiveClick, + showWarningIcon = showWarningIcon, + showInfoIcon = isShowInfoIconPrevState, + apy = tokenStatus.apy.toString(), + ) } - } + computeAndApplyShowInfoIcon(cryptoCurrencyStatus) + }.onLeft { t -> + Timber.e(t) + uiState.update { + YieldSupplyUM.Content( + title = resourceReference( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, + ), + subtitle = resourceReference( + R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle, + ), + rewardsApy = TextReference.EMPTY, + onClick = ::onActiveClick, + showWarningIcon = showWarningIcon, + showInfoIcon = isShowInfoIconPrevState, + apy = "", + ) + } + computeAndApplyShowInfoIcon(cryptoCurrencyStatus) + } } private fun computeAndApplyShowInfoIcon(cryptoCurrencyStatus: CryptoCurrencyStatus) { 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 d167ffb2eb..462e99b59c 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.31.1-1326" +tangemBlockchainSdk = "releases-5.32-1329" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.31-569" +tangemCardSdk = "releases-5.32-574" #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 ^ -tangemHotSdk = "develop-531" +tangemHotSdk = "develop-539" #tangemHotSdk = "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..50802ec815 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -153,19 +153,20 @@ include(":test:core") include(":test:mock") // region Core modules +include(":core:ab-tests") include(":core:analytics") include(":core:analytics:models") -include(":core:datasource") include(":core:config-toggles") -include(":core:ab-tests") -include(":core:navigation") -include(":core:res") -include(":core:ui") -include(":core:utils") +include(":core:datasource") include(":core:decompose") -include(":core:pagination") include(":core:error") include(":core:error:ext") +include(":core:navigation") +include(":core:pagination") +include(":core:res") +include(":core:security") +include(":core:ui") +include(":core:utils") // endregion Core modules // region Common modules @@ -175,6 +176,7 @@ include(":common:routing") include(":common:test") include(":common:ui") include(":common:ui-charts") +include(":common:ui-markets") // endregion // region Libs modules @@ -300,6 +302,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/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"