diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 6acf072e3c..3b22023f6e 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -27,6 +27,7 @@ import com.tangem.datasource.utils.WireMockRedirectInterceptor import com.tangem.domain.promo.PromoRepository import com.tangem.domain.promo.models.PromoId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.tap.MainActivity import dagger.hilt.android.testing.HiltAndroidRule import io.qameta.allure.kotlin.Allure @@ -129,6 +130,12 @@ abstract class BaseTestCase : TestCase( value = false ) } + appPreferencesStore.editData { mutablePreferences -> + mutablePreferences.set( + key = PreferencesKeys.getShouldShowInitialPermissionScreen(PUSH_PERMISSION), + value = false + ) + } promoRepository.setNeverToShowWalletPromo(PromoId.Sepa) } apiEnvironmentRule.setup(apiConfigsManager) diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt index 22a818f96c..49e40ea9f1 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt @@ -128,10 +128,6 @@ fun BaseTestCase.synchronizeAddresses( onMainScreen { totalBalanceText.assert(!hasText(DASH_SIGN)) } } } - - step("Expand 'Main account' to reveal tokens") { - onMainScreen { mainAccount().performClick() } - } } fun BaseTestCase.openDeviceSettingsScreen() { diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt index 43fdc8264d..748c45459c 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt @@ -11,20 +11,17 @@ import io.qameta.allure.kotlin.Allure.step import com.tangem.common.ui.R as CommonUiR private val firstStoryIndex = 0 -private val firstStoryTitle = getResourceString(CommonUiR.string.swap_story_first_title) -private val firstStorySubtitle = getResourceString(CommonUiR.string.swap_story_first_subtitle) +private val firstStoryTitle = getResourceString(CommonUiR.string.swap_story_first_title_v2) +private val firstStorySubtitle = getResourceString(CommonUiR.string.swap_story_first_subtitle_v2) private val secondStoryIndex = 1 -private val secondStoryTitle = getResourceString(CommonUiR.string.swap_story_second_title) -private val secondStorySubtitle = getResourceString(CommonUiR.string.swap_story_second_subtitle) +private val secondStoryTitle = getResourceString(CommonUiR.string.swap_story_second_title_v2) +private val secondStorySubtitle = getResourceString(CommonUiR.string.swap_story_second_subtitle_v2) private val thirdStoryIndex = 2 -private val thirdStoryTitle = getResourceString(CommonUiR.string.swap_story_third_title) -private val thirdStorySubtitle = getResourceString(CommonUiR.string.swap_story_third_subtitle) +private val thirdStoryTitle = getResourceString(CommonUiR.string.swap_story_third_title_v2) +private val thirdStorySubtitle = getResourceString(CommonUiR.string.swap_story_third_subtitle_v2) private val forthStoryIndex = 3 -private val forthStoryTitle = getResourceString(CommonUiR.string.swap_story_forth_title) -private val forthStorySubtitle = getResourceString(CommonUiR.string.swap_story_forth_subtitle) -private val fifthStoryIndex = 4 -private val fifthStoryTitle = getResourceString(CommonUiR.string.swap_story_fifth_title) -private val fifthStorySubtitle = getResourceString(CommonUiR.string.swap_story_fifth_subtitle) +private val forthStoryTitle = getResourceString(CommonUiR.string.swap_story_forth_title_v2) +private val forthStorySubtitle = getResourceString(CommonUiR.string.swap_story_forth_subtitle_v2) fun BaseTestCase.openSwapScreen( from: SwapEntryPoint, @@ -120,26 +117,6 @@ fun BaseTestCase.checkStoriesChanges() { storySubtitle = forthStorySubtitle ) } - step("Click on right side") { - onSwapStoriesScreen { container.performTouchInput { click(centerRight) } } - } - step("Check title and subtitle for story №${fifthStoryIndex + 1}") { - checkStoriesContent( - storyIndex = fifthStoryIndex, - storyTitle = fifthStoryTitle, - storySubtitle = fifthStorySubtitle - ) - } - step("Click on left side") { - onSwapStoriesScreen { container.performTouchInput { click(centerLeft) } } - } - step("Check title and subtitle for story №${forthStoryIndex + 1}") { - checkStoriesContent( - storyIndex = forthStoryIndex, - storyTitle = forthStoryTitle, - storySubtitle = forthStorySubtitle - ) - } step("Click on left side") { onSwapStoriesScreen { container.performTouchInput { click(centerLeft) } } } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt index abdbaeb2a7..d281e57344 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt @@ -244,7 +244,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) fun tokenWithTitleAndAddress(tokenTitle: String): KNode { return lazyList.childWith { hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM) - hasAnyDescendant(withText(tokenTitle)) + hasText(tokenTitle) useUnmergedTree = true }.child { hasTestTag(TokenElementsTestTags.TOKEN_FIAT_AMOUNT) @@ -256,7 +256,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) fun tokenWithCustomDerivationIcon(tokenTitle: String): KNode { return lazyList.childWith { hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM) - hasAnyDescendant(withText(tokenTitle)) + hasText(tokenTitle) useUnmergedTree = true }.child { hasTestTag(TokenElementsTestTags.TOKEN_CUSTOM_DERIVATION_ICON) @@ -297,7 +297,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) fun tokenWithTitleAndPosition(tokenTitle: String, index: Int): KNode { return lazyList.childWith { hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM) - hasAnyDescendant(withText(tokenTitle)) + hasText(tokenTitle) hasLazyListItemPosition(index) useUnmergedTree = true }.child { diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt index 73c09bf564..ce2c554f0f 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt @@ -143,7 +143,7 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) val youSwapBlock: KNode = child { hasTestTag(SwapTokenScreenTestTags.SWAP_BLOCK_HEADER) - hasAnyDescendant(withText(getResourceString(R.string.swapping_from_title))) + hasAnyDescendant(withText(getResourceString(R.string.swapping_from_title_v2))) hasAnyDescendant(withTestTag(SwapTokenScreenTestTags.BALANCE)) useUnmergedTree = true } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapChooseTokenScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapChooseTokenScreenTest.kt index def2f12334..426cb52d6e 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapChooseTokenScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapChooseTokenScreenTest.kt @@ -30,7 +30,6 @@ class SwapChooseTokenScreenTest : BaseTestCase() { @Test fun checkAvailableToSwapTokensListTest() { val tokenTitle = "Polygon" - val inputAmount = "100" val ethereum = "Ethereum" val polExMatic = "POL (ex-MATIC)" val bitcoin = "Bitcoin" @@ -66,13 +65,6 @@ class SwapChooseTokenScreenTest : BaseTestCase() { step("Assert 'You swap' block is displayed") { onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } } - step("Input swap amount = '$inputAmount'") { - waitForIdle() - onSwapTokenScreen { - textInput.clickWithAssertion() - textInput.performTextReplacement(inputAmount) - } - } step("Click on 'Select token' icon") { onSwapTokenScreen { swapSelectTokenIcon.performClick() } } @@ -102,7 +94,6 @@ class SwapChooseTokenScreenTest : BaseTestCase() { @Test fun checkSearchOnSwapChooseTokenScreenTest() { val tokenTitle = "Polygon" - val inputAmount = "100" val ethereum = "Ethereum" val polExMatic = "POL (ex-MATIC)" val polExMaticSymbol = "POL" @@ -129,13 +120,6 @@ class SwapChooseTokenScreenTest : BaseTestCase() { step("Assert 'You swap' block is displayed") { onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } } - step("Input swap amount = '$inputAmount'") { - waitForIdle() - onSwapTokenScreen { - textInput.clickWithAssertion() - textInput.performTextReplacement(inputAmount) - } - } step("Click on 'Choose token' button") { onSwapTokenScreen { chooseTokenButton.performClick() } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapStoriesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapStoriesTest.kt index 9820b6fe9b..13bc7aba06 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapStoriesTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapStoriesTest.kt @@ -117,7 +117,7 @@ class SwapStoriesTest : BaseTestCase() { @DisplayName("Check unavailable swap stories on 'Main' screen") @Test fun checkUnavailableSwapStoriesOnMainScreen() { - val scenarioName = "stories_first_time_swap" + val scenarioName = "stories_first_time_swap_v2" val scenarioErrorState = "Error" val packageName = getTargetContext().packageName @@ -168,7 +168,7 @@ class SwapStoriesTest : BaseTestCase() { @DisplayName("Check unavailable swap stories on 'Token details' screen") @Test fun checkUnavailableSwapStoriesOnTokenDetailsScreen() { - val scenarioName = "stories_first_time_swap" + val scenarioName = "stories_first_time_swap_v2" val scenarioErrorState = "Error" val packageName = getTargetContext().packageName val tokenName = "Ethereum" @@ -224,7 +224,7 @@ class SwapStoriesTest : BaseTestCase() { @DisplayName("Check unavailable swap stories on 'Markets' token details screen") @Test fun checkUnavailableSwapStoriesOnMarketsTokenDetailsScreen() { - val scenarioName = "stories_first_time_swap" + val scenarioName = "stories_first_time_swap_v2" val scenarioErrorState = "Error" val packageName = getTargetContext().packageName val tokenName = "Ethereum" diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt index 0336d3895d..94092b1e2f 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt @@ -68,6 +68,9 @@ class SwapTokenScreenTest : BaseTestCase() { } } } + step("Choose receive token") { + chooseReceiveToken(receiveTokenName) + } step("Input swap amount = '$inputAmount'") { waitForIdle() onSwapTokenScreen { @@ -75,9 +78,6 @@ class SwapTokenScreenTest : BaseTestCase() { textInput.performTextReplacement(inputAmount) } } - step("Choose receive token") { - chooseReceiveToken(receiveTokenName) - } step("Assert input amount = '$inputAmount'") { onSwapTokenScreen { textInput.assertTextEquals(inputAmount) } } @@ -121,6 +121,7 @@ class SwapTokenScreenTest : BaseTestCase() { @Test fun networkErrorSwapTest() { val tokenTitle = "Polygon" + val receiveTokenName = "Ethereum" setupHooks( additionalAfterSection = { @@ -154,6 +155,9 @@ class SwapTokenScreenTest : BaseTestCase() { step("Assert 'Swap' screen title is displayed") { onSwapTokenScreen { title.assertIsDisplayed() } } + step("Choose receive token") { + chooseReceiveToken(receiveTokenName) + } step("Assert error notification title is displayed") { onSwapTokenScreen { waitForIdle() @@ -212,6 +216,9 @@ class SwapTokenScreenTest : BaseTestCase() { } } } + step("Choose receive token") { + chooseReceiveToken(receiveTokenName) + } step("Input swap amount = '$inputAmount'") { waitForIdle() onSwapTokenScreen { @@ -222,9 +229,6 @@ class SwapTokenScreenTest : BaseTestCase() { step("Assert input amount = '$inputAmount'") { onSwapTokenScreen { textInput.assertTextEquals(inputAmount) } } - step("Choose receive token") { - chooseReceiveToken(receiveTokenName) - } step("Assert receive amount is displayed") { onSwapTokenScreen { flakySafely(WAIT_UNTIL_TIMEOUT) { @@ -461,8 +465,12 @@ class SwapTokenScreenTest : BaseTestCase() { onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() } } step("Click on 'Swap tokens on screen' button") { - onSwapTokenScreen { replaceTokensButton.performClick() } - waitForIdle() + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + onSwapTokenScreen { + replaceTokensButton.assertIsEnabled() + replaceTokensButton.performClick() + } + } } step("Assert new swap token symbol: '$receiveTokenSymbol' is displayed") { onSwapTokenScreen { swapTokenSymbol(receiveTokenSymbol).assertIsDisplayed() } @@ -557,6 +565,9 @@ class SwapTokenScreenTest : BaseTestCase() { step("Assert 'You swap' block is displayed") { onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } } + step("Choose receive token") { + chooseReceiveToken(receiveTokenName) + } step("Input swap amount = '$inputAmount'") { waitForIdle() onSwapTokenScreen { @@ -564,9 +575,6 @@ class SwapTokenScreenTest : BaseTestCase() { textInput.performTextReplacement(inputAmount) } } - step("Choose receive token") { - chooseReceiveToken(receiveTokenName) - } step("Select '$market' fee type") { flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { selectFeeType(FeeType.Market, selectedFeeAmount = marketFeeAmount) @@ -623,6 +631,9 @@ class SwapTokenScreenTest : BaseTestCase() { step("Assert 'You swap' block is displayed") { onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } } + step("Choose receive token") { + chooseReceiveToken(receiveTokenName) + } step("Input swap amount = '$inputAmount'") { waitForIdle() onSwapTokenScreen { @@ -630,9 +641,6 @@ class SwapTokenScreenTest : BaseTestCase() { textInput.performTextReplacement(inputAmount) } } - step("Choose receive token") { - chooseReceiveToken(receiveTokenName) - } step("Select '$marketFeeType' fee type") { flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { selectFeeType(feeType = FeeType.Market, feeAmount) @@ -703,6 +711,9 @@ class SwapTokenScreenTest : BaseTestCase() { step("Assert 'You swap' block is displayed") { onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } } + step("Choose receive token") { + chooseReceiveToken(receiveTokenName) + } step("Input swap amount = '$inputAmount'") { waitForIdle() onSwapTokenScreen { @@ -710,9 +721,6 @@ class SwapTokenScreenTest : BaseTestCase() { textInput.performTextReplacement(inputAmount) } } - step("Choose receive token") { - chooseReceiveToken(receiveTokenName) - } step("Assert 'Swap' button is enabled") { flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { onSwapTokenScreen { swapButton.assertIsEnabled() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt index 3b220ce5d5..51e2368332 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt @@ -60,6 +60,9 @@ class SwapTokenScreenWarningsTest : BaseTestCase() { step("Assert 'You swap' block is displayed") { onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } } + step("Choose receive token") { + chooseReceiveToken(receiveTokenTitle) + } step("Input swap amount = '$inputAmount'") { waitForIdle() onSwapTokenScreen { @@ -67,9 +70,6 @@ class SwapTokenScreenWarningsTest : BaseTestCase() { textInput.performTextReplacement(inputAmount) } } - step("Choose receive token") { - chooseReceiveToken(receiveTokenTitle) - } step("Assert 'Insufficient funds' error is displayed") { waitForIdle() onSwapTokenScreen { insufficientFundsErrorTitle.assertIsDisplayed() } @@ -125,6 +125,9 @@ class SwapTokenScreenWarningsTest : BaseTestCase() { step("Assert 'You swap' block is displayed") { onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } } + step("Choose receive token") { + chooseReceiveToken(networkName) + } step("Input swap amount = '$inputAmount'") { waitForIdle() onSwapTokenScreen { @@ -132,9 +135,6 @@ class SwapTokenScreenWarningsTest : BaseTestCase() { textInput.performTextReplacement(inputAmount) } } - step("Choose receive token") { - chooseReceiveToken(networkName) - } step("Check 'Unable to cover '$networkName' fee notification") { flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol) @@ -204,6 +204,9 @@ class SwapTokenScreenWarningsTest : BaseTestCase() { step("Assert 'You swap' block is displayed") { onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } } + step("Choose receive token") { + chooseReceiveToken(receiveTokenName) + } step("Input swap amount = '$inputAmount'") { waitForIdle() onSwapTokenScreen { @@ -211,9 +214,6 @@ class SwapTokenScreenWarningsTest : BaseTestCase() { textInput.performTextReplacement(inputAmount) } } - step("Choose receive token") { - chooseReceiveToken(receiveTokenName) - } step("Assert fiat amount with warning is displayed") { flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { onSwapTokenScreen { @@ -293,6 +293,9 @@ class SwapTokenScreenWarningsTest : BaseTestCase() { step("Assert 'You swap' block is displayed") { onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } } + step("Choose receive token") { + chooseReceiveToken(receiveTokenName) + } step("Input swap amount = '$inputAmount'") { waitForIdle() onSwapTokenScreen { @@ -300,9 +303,6 @@ class SwapTokenScreenWarningsTest : BaseTestCase() { textInput.performTextReplacement(inputAmount) } } - step("Choose receive token") { - chooseReceiveToken(receiveTokenName) - } step("Assert fiat amount with warning is displayed") { onSwapTokenScreen { receiveFiatAmount.assertTextContains("%", substring = true) } } @@ -381,6 +381,9 @@ class SwapTokenScreenWarningsTest : BaseTestCase() { step("Assert 'You swap' block is displayed") { onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } } + step("Choose receive token") { + chooseReceiveToken(receiveTokenName) + } step("Input swap amount = '$inputAmount'") { waitForIdle() onSwapTokenScreen { @@ -388,9 +391,6 @@ class SwapTokenScreenWarningsTest : BaseTestCase() { textInput.performTextReplacement(inputAmount) } } - step("Choose receive token") { - chooseReceiveToken(receiveTokenName) - } step("Assert 'Invalid amount' warning is not displayed") { flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { checkSwapWarning( @@ -450,6 +450,9 @@ class SwapTokenScreenWarningsTest : BaseTestCase() { step("Assert 'You swap' block is displayed") { onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } } + step("Choose receive token") { + chooseReceiveToken(receiveTokenName) + } step("Input swap amount = '$inputAmount'") { waitForIdle() onSwapTokenScreen { @@ -457,9 +460,6 @@ class SwapTokenScreenWarningsTest : BaseTestCase() { textInput.performTextReplacement(inputAmount) } } - step("Choose receive token") { - chooseReceiveToken(receiveTokenName) - } step("Assert 'Invalid amount' warning is not displayed") { flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { checkSwapWarning( @@ -520,6 +520,9 @@ class SwapTokenScreenWarningsTest : BaseTestCase() { step("Assert 'You swap' block is displayed") { onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } } + step("Choose receive token") { + chooseReceiveToken(receiveTokenName) + } step("Input swap amount = '$inputAmount'") { waitForIdle() onSwapTokenScreen { @@ -527,9 +530,6 @@ class SwapTokenScreenWarningsTest : BaseTestCase() { textInput.performTextReplacement(inputAmount) } } - step("Choose receive token") { - chooseReceiveToken(receiveTokenName) - } step("Assert 'Invalid amount' warning is not displayed") { flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { checkSwapWarning( @@ -589,6 +589,9 @@ class SwapTokenScreenWarningsTest : BaseTestCase() { step("Assert 'You swap' block is displayed") { onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } } + step("Choose receive token") { + chooseReceiveToken(receiveTokenName) + } step("Input swap amount = '$inputAmount'") { waitForIdle() onSwapTokenScreen { @@ -596,9 +599,6 @@ class SwapTokenScreenWarningsTest : BaseTestCase() { textInput.performTextReplacement(inputAmount) } } - step("Choose receive token") { - chooseReceiveToken(receiveTokenName) - } step("Assert 'Invalid amount' warning is displayed") { flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { checkSwapWarning( diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 27f8c3c8e9..f6139be58a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -306,6 +306,26 @@ android:host="news" android:scheme="tangem" /> + + + + + + + + + + + + + + + + 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 24e95b0b93..bc13e2a3cd 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.Basic import com.tangem.core.analytics.models.event.SignIn import com.tangem.domain.card.analytics.IntroductionProcess import com.tangem.domain.card.analytics.ParamCardCurrencyConverter @@ -31,6 +32,7 @@ class CardContextInterceptor( is IntroductionProcess.ButtonScanCardLegacy, is SignIn.ScreenOpened, is SignIn.ButtonAddWallet, + is Basic.CardWasScanned, -> false is SignIn.ErrorBiometricUpdated -> !event.isFromUnlockAll else -> true 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 c26c10205c..de16e632ae 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 @@ -7,11 +7,8 @@ import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter -import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.analytics.models.AnalyticsEvent 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.GlobalUiMessageSender @@ -28,7 +25,6 @@ import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.sdk.extensions.localizedDescriptionRes -import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.mainScope import com.tangem.tap.tangemSdkManager @@ -61,6 +57,7 @@ internal class LegacyScanProcessor @Inject constructor( cardId = cardId, allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated, + source = analyticsSource, ) .doOnFailure { error -> onScanFailure(analyticsSource = analyticsSource, error = error, onFailure = {}, onCancel = {}) @@ -85,10 +82,9 @@ internal class LegacyScanProcessor @Inject constructor( val result = tangemSdkManager.scanProduct( cardId = cardId, shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated, + source = analyticsSource, ) - val analyticsEvent = Basic.CardWasScanned(analyticsSource) - result .doOnFailure { error -> scanFailsCounter.onScanFailure( @@ -111,8 +107,6 @@ internal class LegacyScanProcessor @Inject constructor( scanFailsCounter.reset() tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse) - sendAnalytics(analyticsEvent, scanResponse) - onScanSuccess( scanResponse = scanResponse, onProgressStateChange = onProgressStateChange, @@ -122,16 +116,6 @@ internal class LegacyScanProcessor @Inject constructor( } } - private fun sendAnalytics(analyticsEvent: AnalyticsEvent, scanResponse: ScanResponse) { - // this workaround needed to send CardWasScannedEvent without adding a context - val interceptor = CardContextInterceptor(scanResponse) - val params = analyticsEvent.params.toMutableMap() - interceptor.intercept(params) - analyticsEvent.params = params.toMap() - - Analytics.send(analyticsEvent) - } - private suspend inline fun onScanFailure( analyticsSource: AnalyticsParam.ScreensSources, error: TangemError, diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt index ecd2cffeb0..f0ddd9fe6d 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt @@ -18,11 +18,13 @@ internal class DefaultScanCardRepository( allowRequestAccessCodeFromStorage: Boolean, shouldCheckIsAlreadyActivated: Boolean, ): ScanResponse { + @Suppress("UnreachableCode") return when ( val result = tangemSdkManager.scanProduct( cardId = cardId, allowsRequestAccessCodeFromRepository = allowRequestAccessCodeFromStorage, shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated, + source = TODO("Fix when enable NEW_CARD_SCANNING_ENABLED"), ) ) { is CompletionResult.Success -> result.data diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 6211ed6b11..c4a6e936fc 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -18,6 +18,8 @@ import com.tangem.common.usersCode.UserCodeRepository import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.res.getStringSafe import com.tangem.crypto.bip39.DefaultMnemonic import com.tangem.crypto.hdWallet.DerivationPath @@ -25,13 +27,13 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.WithdrawalSignatureResult import com.tangem.domain.visa.model.* import com.tangem.domain.wallets.derivations.derivationStyleProvider -import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.operations.ScanTask import com.tangem.operations.derivation.DerivationTaskResponse @@ -46,6 +48,7 @@ import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.visa.VisaCardActivationResponse import com.tangem.sdk.api.visa.VisaCardActivationTaskMode import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent +import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor import com.tangem.tap.domain.tasks.product.* import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask import com.tangem.tap.domain.tasks.visa.TangemPaySignWithdrawalHashTask @@ -135,6 +138,7 @@ internal class DefaultTangemSdkManager( messageRes: Int?, allowsRequestAccessCodeFromRepository: Boolean, shouldCheckIsAlreadyActivated: Boolean, + source: AnalyticsParam.ScreensSources, ): CompletionResult { val message = Message(resources.getStringSafe(messageRes ?: R.string.initial_message_scan_header)) return coroutineScope { @@ -152,7 +156,7 @@ internal class DefaultTangemSdkManager( ), cardId = cardId, initialMessage = message, - ).also { sendScanResultsToAnalytics(it) } + ).also { sendAnalytics(result = it, source = source) } } } @@ -225,12 +229,24 @@ internal class DefaultTangemSdkManager( .doOnResult { tangemSdk.config.setupForProduct(ProductType.ANY) } } - private fun sendScanResultsToAnalytics(result: CompletionResult) { - if (result is CompletionResult.Failure) { - (result.error as? TangemSdkError)?.let { error -> - Analytics.sendErrorEvent(TangemSdkErrorEvent(error)) + private fun sendAnalytics(result: CompletionResult, source: AnalyticsParam.ScreensSources) { + result + .doOnSuccess { scanResponse -> + // We don't use the standard analytics event enrichment mechanism with card context params here, + // because we've just scanned a new card that may not yet be selected as the current one + val analyticsEvent = Basic.CardWasScanned(source) + val interceptor = CardContextInterceptor(scanResponse) + val params = analyticsEvent.params.toMutableMap() + interceptor.intercept(params) + analyticsEvent.params = params.toMap() + + Analytics.send(event = analyticsEvent) + } + .doOnFailure { tangemError -> + (tangemError as? TangemSdkError)?.let { error -> + Analytics.sendErrorEvent(TangemSdkErrorEvent(error)) + } } - } } override suspend fun derivePublicKeys( diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index bc15466208..0ed38d78c4 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -14,6 +14,7 @@ import com.tangem.common.core.TangemSdkError import com.tangem.common.core.UserCodeRequestPolicy import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.services.InMemoryStorage +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.res.getStringSafe import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey @@ -63,6 +64,7 @@ class MockTangemSdkManager( messageRes: Int?, allowsRequestAccessCodeFromRepository: Boolean, shouldCheckIsAlreadyActivated: Boolean, + source: AnalyticsParam.ScreensSources, ): CompletionResult { if (!MockProvider.isPreset) { val activity = foregroundActivityObserver.foregroundActivity 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 903f85a2e3..411869a6cd 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 @@ -300,11 +300,14 @@ internal class DefaultUserWalletsListRepository( } val scanResponse = unlockMethod.scanResponse ?: run { - when (val res = tangemSdkManagerProvider().scanProduct( + val tangemSdkManager = tangemSdkManagerProvider() + val result = tangemSdkManager.scanProduct( shouldCheckIsAlreadyActivated = false, - )) { + source = unlockMethod.source, + ) + when (result) { is CompletionResult.Failure -> raise(UnlockWalletError.UserCancelled) - is CompletionResult.Success -> res.data + is CompletionResult.Success -> result.data } } 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 7351bc519d..7f32d68de6 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 @@ -709,6 +709,16 @@ internal class ChildFactory @Inject constructor( componentFactory = feedEntryComponentFactory, ) } + is AppRoute.Earn -> { + createComponentChild( + context = context, + params = FeedEntryRoute.Earn( + preselectedEarnType = route.preselectedEarnType, + preselectedNetworkId = route.preselectedNetworkId, + ), + componentFactory = feedEntryComponentFactory, + ) + } } } } diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt index 3e9d268dd5..23e1f1f8d7 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -6,11 +6,13 @@ import com.tangem.common.routing.DeepLinkRoute import com.tangem.common.routing.DeepLinkScheme import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler +import com.tangem.features.feed.entry.deeplink.EarnDeepLinkHandler import com.tangem.features.feed.entry.deeplink.MarketsDeepLinkHandler import com.tangem.features.feed.entry.deeplink.MarketsTokenDetailDeepLinkHandler import com.tangem.features.feed.entry.deeplink.MarketsTokenExchangesDeepLinkHandler import com.tangem.features.feed.entry.deeplink.NewsDeepLinkHandler import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler +import com.tangem.features.feed.entry.deeplink.YieldDeepLinkHandler import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler import com.tangem.features.onramp.deeplink.SellDeepLinkHandler @@ -56,6 +58,8 @@ internal class DeepLinkFactory @Inject constructor( private val marketsTokenExchangesDeepLink: MarketsTokenExchangesDeepLinkHandler.Factory, private val newsDetailsDeepLink: NewsDetailsDeepLinkHandler.Factory, private val newsDeepLink: NewsDeepLinkHandler.Factory, + private val earnDeepLink: EarnDeepLinkHandler.Factory, + private val yieldDeepLink: YieldDeepLinkHandler.Factory, ) { private val permittedAppRoute = MutableStateFlow(false) @@ -162,6 +166,8 @@ internal class DeepLinkFactory @Inject constructor( DeepLinkRoute.Promo.host -> promoDeepLink.create(coroutineScope, queryParams) DeepLinkRoute.OnboardVisa.host -> onboardVisaDeepLink.create(deeplinkUri) DeepLinkRoute.News.host -> newsDeepLink.create(queryParams) + DeepLinkRoute.Earn.host -> earnDeepLink.create(queryParams) + DeepLinkRoute.Yield.host -> yieldDeepLink.create(coroutineScope, queryParams) else -> { TangemLogger.i( """ diff --git a/app/src/mocked/java/com/tangem/tap/data/MockAwareTangemPayStorage.kt b/app/src/mocked/java/com/tangem/tap/data/MockAwareTangemPayStorage.kt index eabe4ccda7..baea0ab1a5 100644 --- a/app/src/mocked/java/com/tangem/tap/data/MockAwareTangemPayStorage.kt +++ b/app/src/mocked/java/com/tangem/tap/data/MockAwareTangemPayStorage.kt @@ -84,10 +84,8 @@ internal class MockAwareTangemPayStorage @Inject constructor( override suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean) = real.storeCheckCustomerWalletResult(userWalletId, isPaeraCustomer) - override suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? { - if (isMockMode) return true - return real.checkCustomerWalletResult(userWalletId) - } + override suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? = + real.checkCustomerWalletResult(userWalletId) override suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) = real.storeActiveWithdrawOrderId(userWalletId, orderId) diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt index 7a1c5173b5..0c50d75372 100644 --- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt @@ -4,11 +4,13 @@ import android.net.Uri import com.tangem.common.routing.AppRoute import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler +import com.tangem.features.feed.entry.deeplink.EarnDeepLinkHandler import com.tangem.features.feed.entry.deeplink.MarketsDeepLinkHandler import com.tangem.features.feed.entry.deeplink.MarketsTokenDetailDeepLinkHandler import com.tangem.features.feed.entry.deeplink.MarketsTokenExchangesDeepLinkHandler import com.tangem.features.feed.entry.deeplink.NewsDeepLinkHandler import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler +import com.tangem.features.feed.entry.deeplink.YieldDeepLinkHandler import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler import com.tangem.features.onramp.deeplink.SellDeepLinkHandler @@ -92,6 +94,14 @@ class DeepLinkFactoryTest { every { create(any()) } returns mockk() } + private val earnDeepLinkFactory = mockk(relaxed = true) { + every { create(any()) } returns mockk() + } + + private val yieldDeepLinkFactory = mockk(relaxed = true) { + every { create(any(), any()) } returns mockk() + } + private val marketsTokenExchangesDeepLinkFactory = mockk(relaxed = true) { every { create(any(), any()) } returns mockk() @@ -122,6 +132,8 @@ class DeepLinkFactoryTest { onboardVisaDeepLink = onboardVisaDeepLink, newsDetailsDeepLink = newsDeeplink, newsDeepLink = newsDeepLinkFactory, + earnDeepLink = earnDeepLinkFactory, + yieldDeepLink = yieldDeepLinkFactory, ) @OptIn(ExperimentalCoroutinesApi::class) @@ -439,18 +451,28 @@ class DeepLinkFactoryTest { } @Test - fun `handleTangemDeepLinks routes news host to dedicated handler`() = runTest { + fun `handleTangemDeepLinks routes news, earn and yield hosts to dedicated handlers`() = runTest { every { mockedUri.scheme } returns "tangem" - every { mockedUri.host } returns "news" every { mockedUri.query } returns null every { mockedUri.queryParameterNames } returns emptySet() every { mockedUri.getQueryParameter(any()) } returns null deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet) + + every { mockedUri.host } returns "news" deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() - verify { newsDeepLinkFactory.create(eq(emptyMap())) } + + every { mockedUri.host } returns "earn" + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) + advanceUntilIdle() + verify { earnDeepLinkFactory.create(eq(emptyMap())) } + + every { mockedUri.host } returns "yield" + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) + advanceUntilIdle() + verify { yieldDeepLinkFactory.create(eq(testScope), eq(emptyMap())) } } @Test 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 cc3ea90fdc..1a41843994 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 @@ -18,6 +18,7 @@ import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.earn.PreselectedEarnType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.serialization.SerializedBigDecimal import com.tangem.domain.models.wallet.UserWalletId @@ -495,4 +496,10 @@ sealed class AppRoute(val path: String) : Route { data class News( val categoryId: Int? = null, ) : AppRoute(path = "/news") + + @Serializable + data class Earn( + val preselectedEarnType: PreselectedEarnType? = null, + val preselectedNetworkId: String? = null, + ) : AppRoute(path = "/earn") } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt index b9caabe5d0..0bdcdc9f27 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt @@ -75,6 +75,14 @@ sealed class DeepLinkRoute { data object News : DeepLinkRoute() { override val host: String = "news" } + + data object Earn : DeepLinkRoute() { + override val host: String = "earn" + } + + data object Yield : DeepLinkRoute() { + override val host: String = "yield" + } } enum class DeepLinkScheme(val scheme: String) { diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt index 8f745a3b32..5196edd70a 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt @@ -20,4 +20,5 @@ object DeeplinkConst { const val SECTION_KEY = "section" const val CATEGORY_ID_KEY = "category_id" const val NEWS_ID_KEY = "news_id" + const val EARN_TYPE_KEY = "earn_type" } \ No newline at end of file diff --git a/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/DeepLinkBuilderTest.kt b/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/DeepLinkBuilderTest.kt index bdcb99e48d..4e7aab6d8b 100644 --- a/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/DeepLinkBuilderTest.kt +++ b/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/DeepLinkBuilderTest.kt @@ -108,6 +108,31 @@ internal class DeepLinkBuilderTest { assertThat(result).isEqualTo("${DeeplinkConst.TANGEM_SCHEME}://news?${DeeplinkConst.NEWS_ID_KEY}=20533") } + @Test + fun `earn host with filters produces expected uri`() { + val result = deepLinkBuilder + .setAction("earn") + .addQueryParam(DeeplinkConst.EARN_TYPE_KEY, "yield") + .addQueryParam(DeeplinkConst.NETWORK_ID_KEY, "base") + .build() + + assertThat(result).isEqualTo( + "${DeeplinkConst.TANGEM_SCHEME}://earn?${DeeplinkConst.EARN_TYPE_KEY}=yield" + + "&${DeeplinkConst.NETWORK_ID_KEY}=base", + ) + } + + @Test + fun `yield host with token and network produces expected uri`() { + val result = deepLinkBuilder + .setAction("yield") + .addQueryParam(DeeplinkConst.TOKEN_ID_KEY, "usd-coin") + .addQueryParam(DeeplinkConst.NETWORK_ID_KEY, "base") + .build() + + assertThat(result).isEqualTo("${DeeplinkConst.TANGEM_SCHEME}://yield?token_id=usd-coin&network_id=base") + } + @Test fun `GIVEN complex deep link WHEN build THEN should construct correct URI`() { // GIVEN 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 d813aed232..3d83030aba 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 @@ -41,7 +41,7 @@ }, { "name": "ASSETS_DISCOVERY_ENABLED", - "version": "undefined" + "version": "5.38" }, { "name": "SOLANA_TX_HISTORY_ENABLED", diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesProgressBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesProgressBar.kt index 00592f5bf2..0ef1136197 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesProgressBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesProgressBar.kt @@ -96,14 +96,14 @@ fun StoriesProgressBar( .height(2.dp) .weight(1f) .clip(RoundedCornerShape(2.dp)) - .background(TangemColorPalette.White.copy(alpha = .2f)), + .background(TangemColorPalette.White.copy(alpha = .2f)) + .testTag(SwapStoriesScreenTestTags.PROGRESS_BAR_ITEM), ) { Box( modifier = Modifier .clip(RoundedCornerShape(2.dp)) .background(TangemColorPalette.White) .fillMaxHeight() - .testTag(SwapStoriesScreenTestTags.PROGRESS_BAR_ITEM) .let { modifier -> when (index) { currentStep -> modifier.fillMaxWidth(progress.value) diff --git a/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareOnboardingRepository.kt b/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareOnboardingRepository.kt index 9bc64e5bcf..38ccbe554a 100644 --- a/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareOnboardingRepository.kt +++ b/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareOnboardingRepository.kt @@ -71,15 +71,11 @@ internal class MockAwareOnboardingRepository @Inject constructor( override suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either = real.hasTangemPayInWallet(userWalletId) - override suspend fun checkCustomerEligibility(): List { - if (isMockMode) return listOf(TangemPayEligibilityType.DETAILS) - return real.checkCustomerEligibility() - } + override suspend fun checkCustomerEligibility(): List = + real.checkCustomerEligibility() - override suspend fun getCustomerEligibility(): List { - if (isMockMode) return listOf(TangemPayEligibilityType.DETAILS) - return real.getCustomerEligibility() - } + override suspend fun getCustomerEligibility(): List = + real.getCustomerEligibility() override fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? = real.getSavedCustomerInfo(userWalletId) diff --git a/domain/common/build.gradle.kts b/domain/common/build.gradle.kts index 20e637a0a5..e1bfa04f51 100644 --- a/domain/common/build.gradle.kts +++ b/domain/common/build.gradle.kts @@ -5,13 +5,14 @@ plugins { } dependencies { - api(deps.kotlin.coroutines) api(deps.arrow.core) api(deps.arrow.fx) - api(projects.domain.models) - + api(deps.kotlin.coroutines) implementation(deps.kotlin.serialization) + api(projects.core.analytics.models) + api(projects.domain.models) + testImplementation(deps.test.coroutine) testImplementation(deps.test.junit) testImplementation(deps.test.mockk) diff --git a/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepository.kt b/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepository.kt index c9f9aecb00..c5e809f26b 100644 --- a/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepository.kt +++ b/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepository.kt @@ -1,6 +1,7 @@ package com.tangem.domain.common.wallets import arrow.core.Either +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.common.wallets.error.* import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet @@ -145,7 +146,10 @@ interface UserWalletsListRepository { sealed class UnlockMethod { data object Biometric : UnlockMethod() data object AccessCode : UnlockMethod() - data class Scan(val scanResponse: ScanResponse? = null) : UnlockMethod() + data class Scan( + val scanResponse: ScanResponse? = null, + val source: AnalyticsParam.ScreensSources, + ) : UnlockMethod() } } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/PreselectedEarnType.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/PreselectedEarnType.kt new file mode 100644 index 0000000000..51d961c48a --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/PreselectedEarnType.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.models.earn + +import kotlinx.serialization.Serializable + +@Serializable +enum class PreselectedEarnType(val value: String) { + Staking("staking"), + Yield("yield"), + ; + + companion object { + fun parse(value: String?): PreselectedEarnType? = entries.firstOrNull { it.value == value } + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/NonBiometricUnlockWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/NonBiometricUnlockWalletUseCase.kt index d225546c2d..d55d30a84f 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/NonBiometricUnlockWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/NonBiometricUnlockWalletUseCase.kt @@ -2,6 +2,7 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either import arrow.core.raise.either +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.error.UnlockWalletError import com.tangem.domain.models.wallet.UserWallet @@ -22,7 +23,10 @@ class NonBiometricUnlockWalletUseCase( private val walletsRepository: WalletsRepository, ) { - suspend operator fun invoke(userWalletId: UserWalletId): Either = either { + suspend operator fun invoke( + userWalletId: UserWalletId, + source: AnalyticsParam.ScreensSources, + ): Either = either { val userWallet = userWalletsListRepository.userWalletsSync() .find { it.walletId == userWalletId } ?: raise(UnlockWalletError.UserWalletNotFound) @@ -32,7 +36,7 @@ class NonBiometricUnlockWalletUseCase( } val method = when (userWallet) { - is UserWallet.Cold -> UserWalletsListRepository.UnlockMethod.Scan() + is UserWallet.Cold -> UserWalletsListRepository.UnlockMethod.Scan(scanResponse = null, source = source) is UserWallet.Hot -> UserWalletsListRepository.UnlockMethod.AccessCode } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletUseCase.kt index 61d4c4e10c..7d92dd628e 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletUseCase.kt @@ -2,6 +2,7 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either import arrow.core.raise.either +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.error.UnlockWalletError import com.tangem.domain.models.wallet.UserWalletId @@ -19,12 +20,15 @@ class UnlockWalletUseCase( private val nonBiometricUnlockWalletUseCase: NonBiometricUnlockWalletUseCase, ) { - suspend operator fun invoke(userWalletId: UserWalletId): Either = either { + suspend operator fun invoke( + userWalletId: UserWalletId, + source: AnalyticsParam.ScreensSources, + ): Either = either { userWalletsListRepository.unlock(userWalletId, UserWalletsListRepository.UnlockMethod.Biometric) .mapLeft { error -> when (error) { UnlockWalletError.AlreadyUnlocked -> Unit - else -> nonBiometricUnlockWalletUseCase(userWalletId).bind() + else -> nonBiometricUnlockWalletUseCase(userWalletId, source).bind() } } } 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 afddd1aad4..125cf4eb0c 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 @@ -227,7 +227,10 @@ internal class CreateWalletStartModel @Inject constructor( is SaveWalletError.WalletAlreadySaved -> { userWalletsListRepository.unlock( userWalletId = userWallet.walletId, - unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), + unlockMethod = UserWalletsListRepository.UnlockMethod.Scan( + scanResponse = scanResponse, + source = AnalyticsParam.ScreensSources.Intro, + ), ).onRight { appRouter.replaceAll(AppRoute.Wallet) } diff --git a/features/create-wallet-start/impl/src/test/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModelTest.kt b/features/create-wallet-start/impl/src/test/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModelTest.kt index 6da464c113..0673e8d9f2 100644 --- a/features/create-wallet-start/impl/src/test/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModelTest.kt +++ b/features/create-wallet-start/impl/src/test/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModelTest.kt @@ -421,7 +421,10 @@ internal class CreateWalletStartModelTest { coVerify { userWalletsListRepository.unlock( userWalletId = testUserWalletId, - unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(testScanResponse), + unlockMethod = UserWalletsListRepository.UnlockMethod.Scan( + scanResponse = testScanResponse, + source = AnalyticsParam.ScreensSources.Intro, + ), ) } verify { appRouter.replaceAll(routes = arrayOf(AppRoute.Wallet), onComplete = any()) } 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 ac74369033..248d94b21a 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 @@ -13,22 +13,22 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.settings.HotWalletRestrictionManager import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.domain.wallets.usecase.ApplyUserWalletListSortingUseCase import com.tangem.domain.wallets.usecase.UnlockWalletUseCase import com.tangem.features.details.entity.UserWalletListUM import com.tangem.features.details.entity.WalletReorderUM import com.tangem.features.details.impl.R -import com.tangem.domain.settings.HotWalletRestrictionManager import com.tangem.features.details.utils.UserWalletSaver import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -112,7 +112,7 @@ internal class UserWalletListModel @Inject constructor( private fun onWalletClicked(userWalletId: UserWalletId) { modelScope.launch { - unlockWalletUseCase(userWalletId) + unlockWalletUseCase(userWalletId, AnalyticsParam.ScreensSources.Settings) .onRight { router.push(AppRoute.WalletSettings(userWalletId)) } .onLeft { error -> TangemLogger.e("Failed to unlock wallet $userWalletId: $error") diff --git a/features/feed/api/build.gradle.kts b/features/feed/api/build.gradle.kts index 890d5aefad..b0a3a10ffc 100644 --- a/features/feed/api/build.gradle.kts +++ b/features/feed/api/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { /* Project - Domain */ implementation(projects.domain.core) + implementation(projects.domain.models) implementation(projects.domain.tokens.models) implementation(projects.domain.appCurrency.models) implementation(projects.domain.markets.models) diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryRoute.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryRoute.kt index 098b29f4da..38225e22f9 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryRoute.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryRoute.kt @@ -5,6 +5,7 @@ import com.tangem.domain.markets.PreselectedMarketsInterval import com.tangem.domain.markets.PreselectedMarketsOrder import com.tangem.domain.markets.PreselectedTokenDetailsSection import com.tangem.domain.markets.TokenMarketParams +import com.tangem.domain.models.earn.PreselectedEarnType import kotlinx.serialization.Serializable @Serializable @@ -39,4 +40,10 @@ sealed interface FeedEntryRoute { @Serializable data class NewsList(val preselectedCategoryId: Int? = null) : FeedEntryRoute + + @Serializable + data class Earn( + val preselectedEarnType: PreselectedEarnType? = null, + val preselectedNetworkId: String? = null, + ) : FeedEntryRoute } \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/EarnDeepLinkHandler.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/EarnDeepLinkHandler.kt new file mode 100644 index 0000000000..01c17aed53 --- /dev/null +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/EarnDeepLinkHandler.kt @@ -0,0 +1,8 @@ +package com.tangem.features.feed.entry.deeplink + +interface EarnDeepLinkHandler { + + interface Factory { + fun create(queryParams: Map): EarnDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/YieldDeepLinkHandler.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/YieldDeepLinkHandler.kt new file mode 100644 index 0000000000..5512452cb0 --- /dev/null +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/YieldDeepLinkHandler.kt @@ -0,0 +1,10 @@ +package com.tangem.features.feed.entry.deeplink + +import kotlinx.coroutines.CoroutineScope + +interface YieldDeepLinkHandler { + + interface Factory { + fun create(coroutineScope: CoroutineScope, queryParams: Map): YieldDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts index 7d4dc84f32..a9908d1787 100644 --- a/features/feed/impl/build.gradle.kts +++ b/features/feed/impl/build.gradle.kts @@ -112,4 +112,5 @@ dependencies { testRuntimeOnly(deps.test.junit5.engine) testImplementation(deps.test.mockk) testImplementation(deps.test.truth) + testImplementation(deps.test.coroutine) } \ No newline at end of file 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 198cdac711..94c272503e 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 @@ -21,10 +21,13 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams +import com.tangem.domain.models.earn.PreselectedEarnType import com.tangem.domain.news.model.NewsListConfig +import com.tangem.features.feed.components.earn.DefaultEarnComponent 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 com.tangem.features.feed.entry.components.FeedEntryComponent import com.tangem.features.feed.entry.components.FeedEntryRoute import com.tangem.features.feed.model.FeedEntryModel @@ -125,11 +128,19 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( } override fun onOpenAllNews() { - innerRouter.push(FeedEntryChildFactory.Child.NewsList()) + innerRouter.push( + FeedEntryChildFactory.Child.NewsList( + params = buildNewsListParams(onBack = { onChildBack() }), + ), + ) } override fun onOpenEarnPage() { - innerRouter.push(FeedEntryChildFactory.Child.Earn) + innerRouter.push( + FeedEntryChildFactory.Child.Earn( + params = buildEarnParams(onBack = { onChildBack() }), + ), + ) } override fun openSearch(source: String) { @@ -264,12 +275,49 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( ), ) is FeedEntryRoute.NewsList -> FeedEntryChildFactory.Child.NewsList( - preselectedCategoryId = entryRoute.preselectedCategoryId, + params = buildNewsListParams( + onBack = { router.pop() }, + preselectedCategoryId = entryRoute.preselectedCategoryId, + ), + ) + is FeedEntryRoute.Earn -> FeedEntryChildFactory.Child.Earn( + params = buildEarnParams( + onBack = { router.pop() }, + preselectedEarnType = entryRoute.preselectedEarnType, + preselectedNetworkId = entryRoute.preselectedNetworkId, + ), ) null -> FeedEntryChildFactory.Child.Feed } } + private fun buildNewsListParams( + onBack: () -> Unit, + preselectedCategoryId: Int? = null, + ): DefaultNewsListComponent.Params = DefaultNewsListComponent.Params( + onArticleClicked = { currentArticle, prefetchedArticles, paginationConfig -> + clickIntents.onArticleClick( + articleId = currentArticle, + preselectedArticlesId = prefetchedArticles, + screenSource = AnalyticsParam.ScreensSources.NewsList, + paginationConfig = paginationConfig, + ) + }, + onBackClick = onBack, + preselectedCategoryId = preselectedCategoryId, + ) + + private fun buildEarnParams( + onBack: () -> Unit, + preselectedEarnType: PreselectedEarnType? = null, + preselectedNetworkId: String? = null, + ): DefaultEarnComponent.Params = DefaultEarnComponent.Params( + onBackClick = onBack, + onSearchClicked = clickIntents::openSearch, + preselectedEarnType = preselectedEarnType, + preselectedNetworkId = preselectedNetworkId, + ) + @AssistedFactory interface Factory : FeedEntryComponent.Factory { override fun create(context: AppComponentContext, entryRoute: FeedEntryRoute?): DefaultFeedEntryComponent 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 fb8c3a7f90..4ae731f39c 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 @@ -17,7 +17,6 @@ import com.tangem.features.feed.components.market.details.portfolioblock.Portfol 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 com.tangem.features.feed.components.news.list.DefaultNewsListComponent.Params import com.tangem.features.feed.components.search.DefaultSearchComponent import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles import com.tangem.features.promobanners.api.PromoBannersBlockComponent @@ -53,7 +52,7 @@ internal class FeedEntryChildFactory @Inject constructor( @Serializable @Immutable - data class NewsList(val preselectedCategoryId: Int? = null) : Child + data class NewsList(val params: DefaultNewsListComponent.Params) : Child @Serializable @Immutable @@ -61,7 +60,7 @@ internal class FeedEntryChildFactory @Inject constructor( @Serializable @Immutable - data object Earn : Child + data class Earn(val params: DefaultEarnComponent.Params) : Child @Serializable @Immutable @@ -113,18 +112,7 @@ internal class FeedEntryChildFactory @Inject constructor( is Child.NewsList -> { DefaultNewsListComponent( appComponentContext = appComponentContext, - params = Params( - onArticleClicked = { currentArticle, prefetchedArticles, paginationConfig -> - feedEntryClickIntents.onArticleClick( - articleId = currentArticle, - preselectedArticlesId = prefetchedArticles, - screenSource = AnalyticsParam.ScreensSources.NewsList, - paginationConfig = paginationConfig, - ) - }, - onBackClick = onBackClicked, - preselectedCategoryId = child.preselectedCategoryId, - ), + params = child.params, ) } Child.Feed -> { @@ -139,10 +127,7 @@ internal class FeedEntryChildFactory @Inject constructor( is Child.Earn -> { DefaultEarnComponent( appComponentContext = appComponentContext, - params = DefaultEarnComponent.Params( - onBackClick = onBackClicked, - onSearchClicked = feedEntryClickIntents::openSearch, - ), + params = child.params, addToPortfolioComponentFactory = addToPortfolioComponentFactory, ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt index bdef4f7ffe..9e4094404f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt @@ -34,7 +34,9 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent +import com.tangem.domain.models.earn.PreselectedEarnType import com.tangem.features.feed.components.feed.FeedBottomSheetRoute +import kotlinx.serialization.Serializable import com.tangem.features.feed.model.earn.EarnModel import com.tangem.features.feed.model.earn.analytics.EarnSource import com.tangem.features.feed.ui.components.FeedSearchBar @@ -151,8 +153,11 @@ internal class DefaultEarnComponent( ) } + @Serializable data class Params( val onBackClick: () -> Unit, val onSearchClicked: (source: String) -> Unit, + val preselectedEarnType: PreselectedEarnType? = null, + val preselectedNetworkId: String? = null, ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultEarnDeepLinkHandler.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultEarnDeepLinkHandler.kt new file mode 100644 index 0000000000..4609b29cfb --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultEarnDeepLinkHandler.kt @@ -0,0 +1,38 @@ +package com.tangem.features.feed.deeplink + +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.deeplink.DeeplinkConst.EARN_TYPE_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY +import com.tangem.domain.models.earn.PreselectedEarnType +import com.tangem.features.feed.entry.deeplink.EarnDeepLinkHandler +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultEarnDeepLinkHandler @AssistedInject constructor( + @Assisted private val queryParams: Map, + private val appRouter: AppRouter, +) : EarnDeepLinkHandler { + + init { + handleDeepLink() + } + + private fun handleDeepLink() { + val earnType = PreselectedEarnType.parse(queryParams[EARN_TYPE_KEY]) + val networkId = queryParams[NETWORK_ID_KEY]?.takeIf { it.isNotBlank() } + + appRouter.push( + AppRoute.Earn( + preselectedEarnType = earnType, + preselectedNetworkId = networkId, + ), + ) + } + + @AssistedFactory + interface Factory : EarnDeepLinkHandler.Factory { + override fun create(queryParams: Map): DefaultEarnDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultYieldDeepLinkHandler.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultYieldDeepLinkHandler.kt new file mode 100644 index 0000000000..ee24674898 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultYieldDeepLinkHandler.kt @@ -0,0 +1,107 @@ +package com.tangem.features.feed.deeplink + +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY +import com.tangem.domain.models.earn.PreselectedEarnType +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.yield.supply.models.YieldSupplyAvailability +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetAvailabilityUseCase +import com.tangem.features.feed.entry.deeplink.YieldDeepLinkHandler +import com.tangem.utils.logging.TangemLogger +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +internal class DefaultYieldDeepLinkHandler @AssistedInject constructor( + @Assisted private val scope: CoroutineScope, + @Assisted private val queryParams: Map, + private val appRouter: AppRouter, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, + private val yieldSupplyGetAvailabilityUseCase: YieldSupplyGetAvailabilityUseCase, +) : YieldDeepLinkHandler { + + init { + handleDeepLink() + } + + private fun handleDeepLink() { + val tokenId = queryParams[TOKEN_ID_KEY]?.takeIf { it.isNotBlank() } + val networkId = queryParams[NETWORK_ID_KEY]?.takeIf { it.isNotBlank() } + + if (tokenId == null || networkId == null) { + TangemLogger.i("Yield deeplink: missing token_id or network_id; falling back to earn yield list") + pushFallback(networkId) + return + } + + scope.launch { + val userWallet = getSelectedWalletSyncUseCase().getOrNull() + if (userWallet == null) { + TangemLogger.e("Yield deeplink: no selected user wallet") + pushFallback(networkId) + return@launch + } + + val cryptoCurrency = multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWallet.walletId), + ) + .orEmpty() + .firstOrNull { currency -> + currency.network.rawId.equals(networkId, ignoreCase = true) && + currency.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true + } + + if (cryptoCurrency == null) { + TangemLogger.i( + """ + Yield deeplink: token not in selected wallet + |- $TOKEN_ID_KEY: $tokenId + |- $NETWORK_ID_KEY: $networkId + """.trimIndent(), + ) + pushFallback(networkId) + return@launch + } + + val availability = yieldSupplyGetAvailabilityUseCase(cryptoCurrency).getOrNull() + val available = availability as? YieldSupplyAvailability.Available + if (available == null) { + TangemLogger.i("Yield deeplink: yield not available for ${cryptoCurrency.name}") + pushFallback(networkId) + return@launch + } + + appRouter.push( + AppRoute.YieldSupplyEntry( + userWalletId = userWallet.walletId, + cryptoCurrency = cryptoCurrency, + apy = available.apy, + ), + ) + } + } + + private fun pushFallback(networkId: String?) { + appRouter.push( + AppRoute.Earn( + preselectedEarnType = PreselectedEarnType.Yield, + preselectedNetworkId = networkId, + ), + ) + } + + @AssistedFactory + interface Factory : YieldDeepLinkHandler.Factory { + override fun create( + coroutineScope: CoroutineScope, + queryParams: Map, + ): DefaultYieldDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/FeedDeepLinkModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/FeedDeepLinkModule.kt index 05f42eb535..0af064279f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/FeedDeepLinkModule.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/FeedDeepLinkModule.kt @@ -1,9 +1,13 @@ package com.tangem.features.feed.deeplink.di +import com.tangem.features.feed.deeplink.DefaultEarnDeepLinkHandler import com.tangem.features.feed.deeplink.DefaultNewsDeepLinkHandler import com.tangem.features.feed.deeplink.DefaultNewsDetailsDeepLinkHandler +import com.tangem.features.feed.deeplink.DefaultYieldDeepLinkHandler +import com.tangem.features.feed.entry.deeplink.EarnDeepLinkHandler import com.tangem.features.feed.entry.deeplink.NewsDeepLinkHandler import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler +import com.tangem.features.feed.entry.deeplink.YieldDeepLinkHandler import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -23,4 +27,12 @@ internal interface FeedDeepLinkModule { @Binds @Singleton fun bindNewsDeepLinkHandlerFactory(impl: DefaultNewsDeepLinkHandler.Factory): NewsDeepLinkHandler.Factory + + @Binds + @Singleton + fun bindEarnDeepLinkHandlerFactory(impl: DefaultEarnDeepLinkHandler.Factory): EarnDeepLinkHandler.Factory + + @Binds + @Singleton + fun bindYieldDeepLinkHandlerFactory(impl: DefaultYieldDeepLinkHandler.Factory): YieldDeepLinkHandler.Factory } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index d795a1289d..ae1280b2cf 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -23,6 +23,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.earn.EarnNetworks import com.tangem.domain.models.earn.EarnTokenWithCurrency import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager +import com.tangem.domain.models.earn.PreselectedEarnType import com.tangem.features.feed.components.earn.DefaultEarnComponent import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent import com.tangem.features.feed.components.earn.EarnTypeFilterComponent @@ -108,7 +109,8 @@ internal class EarnModel @Inject constructor( init { updateInitialState() fetchEarnNetworks() - subscribeOnStoredFilters() + fetchTopEarnTokens() + subscribeOnActiveFilters() subscribeOnNetworks() subscribeOnBatchFlow() subscribeToMostlyUsed() @@ -147,6 +149,45 @@ internal class EarnModel @Inject constructor( ) } + private fun activeFilters(): Flow { + val deeplink = buildDeeplinkFilter() ?: return getEarnFilterUseCase() + return getEarnFilterUseCase().drop(1).onStart { emit(deeplink) } + } + + private fun buildDeeplinkFilter(): EarnFilter? { + val type = params.preselectedEarnType?.toEarnFilterType() + val networkId = params.preselectedNetworkId?.takeIf { it.isNotBlank() } + if (type == null && networkId == null) return null + return EarnFilter( + earnFilterType = type ?: EarnFilterType.ALL, + earnFilterNetwork = networkId + ?.let { EarnFilterNetwork.Specific(id = it, symbol = "", fullName = it, isSelected = true) } + ?: EarnFilterNetwork.AllNetworks(isSelected = true), + ) + } + + private fun EarnFilter.resolveAgainst(networks: EarnNetworks): EarnFilter { + val specific = earnFilterNetwork as? EarnFilterNetwork.Specific ?: return this + if (specific.symbol.isNotEmpty()) return this + val loaded = networks.getOrNull()?.takeIf { it.isNotEmpty() } ?: return this + val match = loaded.firstOrNull { it.networkId.equals(specific.id, ignoreCase = true) } + return copy( + earnFilterNetwork = match?.let { earnNetwork -> + EarnFilterNetwork.Specific( + isSelected = true, + id = earnNetwork.networkId, + symbol = earnNetwork.symbol, + fullName = earnNetwork.fullName, + ) + } ?: EarnFilterNetwork.AllNetworks(isSelected = true), + ) + } + + private fun PreselectedEarnType.toEarnFilterType(): EarnFilterType = when (this) { + PreselectedEarnType.Staking -> EarnFilterType.STAKING + PreselectedEarnType.Yield -> EarnFilterType.YIELD + } + private fun subscribeOnBatchFlow() { combine( batchFlowManager.uiItems, @@ -193,18 +234,14 @@ internal class EarnModel @Inject constructor( } } - private fun subscribeOnStoredFilters() { + private fun subscribeOnActiveFilters() { modelScope.launch(dispatchers.default) { - combine( - getEarnFilterUseCase(), - earnNetworks, - ) { filter, networks -> - val typeFilterUM = EarnFilterTypeConverter().convert(filter.earnFilterType) - val networkFilterUM = EarnFilterNetworkConverter().convert(filter.earnFilterNetwork) + combine(activeFilters(), earnNetworks) { filter, networks -> + val resolved = filter.resolveAgainst(networks) stateController.update( EarnFilterSelectedStateTransformer( - filterType = typeFilterUM, - filterNetwork = networkFilterUM, + filterType = EarnFilterTypeConverter().convert(resolved.earnFilterType), + filterNetwork = EarnFilterNetworkConverter().convert(resolved.earnFilterNetwork), earnNetworks = networks, ), ) diff --git a/features/feed/impl/src/test/kotlin/com/tangem/features/feed/deeplink/DefaultEarnDeepLinkHandlerTest.kt b/features/feed/impl/src/test/kotlin/com/tangem/features/feed/deeplink/DefaultEarnDeepLinkHandlerTest.kt new file mode 100644 index 0000000000..bdaf14d99c --- /dev/null +++ b/features/feed/impl/src/test/kotlin/com/tangem/features/feed/deeplink/DefaultEarnDeepLinkHandlerTest.kt @@ -0,0 +1,129 @@ +package com.tangem.features.feed.deeplink + +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.deeplink.DeeplinkConst.EARN_TYPE_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY +import com.tangem.domain.models.earn.PreselectedEarnType +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class DefaultEarnDeepLinkHandlerTest { + + private val appRouter: AppRouter = mockk() + + @BeforeEach + fun setUp() { + every { appRouter.push(any(), any()) } just Runs + } + + @Test + fun `no params opens earn list with no filters`() { + DefaultEarnDeepLinkHandler(queryParams = emptyMap(), appRouter = appRouter) + + verify { + appRouter.push( + AppRoute.Earn(preselectedEarnType = null, preselectedNetworkId = null), + any(), + ) + } + } + + @Test + fun `staking earnType is parsed`() { + DefaultEarnDeepLinkHandler( + queryParams = mapOf(EARN_TYPE_KEY to "staking"), + appRouter = appRouter, + ) + + verify { + appRouter.push( + AppRoute.Earn(preselectedEarnType = PreselectedEarnType.Staking, preselectedNetworkId = null), + any(), + ) + } + } + + @Test + fun `yield earnType is parsed`() { + DefaultEarnDeepLinkHandler( + queryParams = mapOf(EARN_TYPE_KEY to "yield"), + appRouter = appRouter, + ) + + verify { + appRouter.push( + AppRoute.Earn(preselectedEarnType = PreselectedEarnType.Yield, preselectedNetworkId = null), + any(), + ) + } + } + + @Test + fun `invalid earnType is silently dropped`() { + DefaultEarnDeepLinkHandler( + queryParams = mapOf(EARN_TYPE_KEY to "bogus"), + appRouter = appRouter, + ) + + verify { + appRouter.push( + AppRoute.Earn(preselectedEarnType = null, preselectedNetworkId = null), + any(), + ) + } + } + + @Test + fun `networkId is passed through`() { + DefaultEarnDeepLinkHandler( + queryParams = mapOf(NETWORK_ID_KEY to "base"), + appRouter = appRouter, + ) + + verify { + appRouter.push( + AppRoute.Earn(preselectedEarnType = null, preselectedNetworkId = "base"), + any(), + ) + } + } + + @Test + fun `blank networkId is dropped`() { + DefaultEarnDeepLinkHandler( + queryParams = mapOf(NETWORK_ID_KEY to " "), + appRouter = appRouter, + ) + + verify { + appRouter.push( + AppRoute.Earn(preselectedEarnType = null, preselectedNetworkId = null), + any(), + ) + } + } + + @Test + fun `earnType and networkId together`() { + DefaultEarnDeepLinkHandler( + queryParams = mapOf(EARN_TYPE_KEY to "yield", NETWORK_ID_KEY to "base"), + appRouter = appRouter, + ) + + verify { + appRouter.push( + AppRoute.Earn( + preselectedEarnType = PreselectedEarnType.Yield, + preselectedNetworkId = "base", + ), + any(), + ) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/test/kotlin/com/tangem/features/feed/deeplink/DefaultYieldDeepLinkHandlerTest.kt b/features/feed/impl/src/test/kotlin/com/tangem/features/feed/deeplink/DefaultYieldDeepLinkHandlerTest.kt new file mode 100644 index 0000000000..b9f6992504 --- /dev/null +++ b/features/feed/impl/src/test/kotlin/com/tangem/features/feed/deeplink/DefaultYieldDeepLinkHandlerTest.kt @@ -0,0 +1,247 @@ +package com.tangem.features.feed.deeplink + +import arrow.core.Either +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.earn.PreselectedEarnType +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.yield.supply.models.YieldSupplyAvailability +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetAvailabilityUseCase +import com.tangem.utils.logging.TangemLogger +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.verify +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class DefaultYieldDeepLinkHandlerTest { + + private val appRouter: AppRouter = mockk() + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase = mockk() + private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier = mockk() + private val yieldSupplyGetAvailabilityUseCase: YieldSupplyGetAvailabilityUseCase = mockk() + + private val tokenId = "usd-coin" + private val networkId = "base" + + @BeforeEach + fun setUp() { + mockkObject(TangemLogger) + every { appRouter.push(any(), any()) } just Runs + } + + @Test + fun `missing token_id falls back to earn yield`() = runTest { + handle( + scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)), + queryParams = mapOf(NETWORK_ID_KEY to networkId), + ) + + verify { + appRouter.push( + AppRoute.Earn( + preselectedEarnType = PreselectedEarnType.Yield, + preselectedNetworkId = networkId, + ), + any(), + ) + } + } + + @Test + fun `missing network_id falls back to earn yield`() = runTest { + handle( + scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)), + queryParams = mapOf(TOKEN_ID_KEY to tokenId), + ) + + verify { + appRouter.push( + AppRoute.Earn( + preselectedEarnType = PreselectedEarnType.Yield, + preselectedNetworkId = null, + ), + any(), + ) + } + } + + @Test + fun `no selected wallet falls back to earn yield`() = runTest { + every { getSelectedWalletSyncUseCase() } returns Either.Left(mockk()) + + handle( + scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)), + queryParams = mapOf(TOKEN_ID_KEY to tokenId, NETWORK_ID_KEY to networkId), + ) + advanceUntilIdle() + + verify { + appRouter.push( + AppRoute.Earn( + preselectedEarnType = PreselectedEarnType.Yield, + preselectedNetworkId = networkId, + ), + any(), + ) + } + } + + @Test + fun `token not in wallet falls back to earn yield`() = runTest { + val walletId = UserWalletId("011") + val wallet = mockk { every { this@mockk.walletId } returns walletId } + every { getSelectedWalletSyncUseCase() } returns Either.Right(wallet) + coEvery { + multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + MultiWalletCryptoCurrenciesProducer.Params(walletId), + ) + } returns emptySet() + + handle( + scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)), + queryParams = mapOf(TOKEN_ID_KEY to tokenId, NETWORK_ID_KEY to networkId), + ) + advanceUntilIdle() + + verify { + appRouter.push( + AppRoute.Earn( + preselectedEarnType = PreselectedEarnType.Yield, + preselectedNetworkId = networkId, + ), + any(), + ) + } + } + + @Test + fun `eligible token with yield available pushes YieldSupplyEntry`() = runTest { + val walletId = UserWalletId("011") + val wallet = mockk { every { this@mockk.walletId } returns walletId } + val currency = mockEligibleCurrency() + every { getSelectedWalletSyncUseCase() } returns Either.Right(wallet) + coEvery { + multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + MultiWalletCryptoCurrenciesProducer.Params(walletId), + ) + } returns setOf(currency) + coEvery { yieldSupplyGetAvailabilityUseCase(currency) } returns + Either.Right(YieldSupplyAvailability.Available(apy = "2.66")) + + handle( + scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)), + queryParams = mapOf(TOKEN_ID_KEY to tokenId, NETWORK_ID_KEY to networkId), + ) + advanceUntilIdle() + + verify { + appRouter.push( + match { + it.userWalletId == walletId && it.cryptoCurrency === currency && it.apy == "2.66" + }, + any(), + ) + } + } + + @Test + fun `eligible token with yield unavailable falls back to earn yield`() = runTest { + val walletId = UserWalletId("011") + val wallet = mockk { every { this@mockk.walletId } returns walletId } + val currency = mockEligibleCurrency() + every { getSelectedWalletSyncUseCase() } returns Either.Right(wallet) + coEvery { + multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + MultiWalletCryptoCurrenciesProducer.Params(walletId), + ) + } returns setOf(currency) + coEvery { yieldSupplyGetAvailabilityUseCase(currency) } returns + Either.Right(YieldSupplyAvailability.Unavailable) + + handle( + scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)), + queryParams = mapOf(TOKEN_ID_KEY to tokenId, NETWORK_ID_KEY to networkId), + ) + advanceUntilIdle() + + verify { + appRouter.push( + AppRoute.Earn( + preselectedEarnType = PreselectedEarnType.Yield, + preselectedNetworkId = networkId, + ), + any(), + ) + } + } + + @Test + fun `eligible token with availability lookup error falls back to earn yield`() = runTest { + val walletId = UserWalletId("011") + val wallet = mockk { every { this@mockk.walletId } returns walletId } + val currency = mockEligibleCurrency() + every { getSelectedWalletSyncUseCase() } returns Either.Right(wallet) + coEvery { + multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + MultiWalletCryptoCurrenciesProducer.Params(walletId), + ) + } returns setOf(currency) + coEvery { yieldSupplyGetAvailabilityUseCase(currency) } returns + Either.Left(IllegalStateException("api 500")) + + handle( + scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)), + queryParams = mapOf(TOKEN_ID_KEY to tokenId, NETWORK_ID_KEY to networkId), + ) + advanceUntilIdle() + + verify { + appRouter.push( + AppRoute.Earn( + preselectedEarnType = PreselectedEarnType.Yield, + preselectedNetworkId = networkId, + ), + any(), + ) + } + } + + /** Builds a [CryptoCurrency] mock whose `network.rawId` and `id.rawCurrencyId` match the test's + * [tokenId] / [networkId] — i.e. the supplier-lookup predicate inside the handler returns it. */ + private fun mockEligibleCurrency(): CryptoCurrency = mockk(relaxed = true) { + every { network } returns mockk(relaxed = true) { every { rawId } returns networkId } + every { id } returns mockk(relaxed = true) { + every { rawCurrencyId } returns CryptoCurrency.RawID(tokenId) + } + } + + private fun handle(scope: CoroutineScope, queryParams: Map) { + DefaultYieldDeepLinkHandler( + scope = scope, + queryParams = queryParams, + appRouter = appRouter, + getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, + yieldSupplyGetAvailabilityUseCase = yieldSupplyGetAvailabilityUseCase, + ) + } +} \ No newline at end of file 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 c11626ce87..89da07442c 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 @@ -29,12 +29,12 @@ import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.hotwallet.createhardwarewallet.entity.CreateHardwareWalletUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import com.tangem.utils.logging.TangemLogger import javax.inject.Inject private const val HIDE_PROGRESS_DELAY = 400L @@ -181,7 +181,10 @@ internal class CreateHardwareWalletModel @Inject constructor( ) userWalletsListRepository.unlock( userWalletId = walletId, - unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), + unlockMethod = UserWalletsListRepository.UnlockMethod.Scan( + scanResponse = scanResponse, + source = AnalyticsParam.ScreensSources.AddNew, + ), ).onRight { router.replaceAll(AppRoute.Wallet) } 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 5b9efafb88..09f0e2f4a2 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 @@ -115,7 +115,10 @@ internal class UpgradeWalletModel @Inject constructor( ) tangemSdkManager - .scanProduct(shouldCheckIsAlreadyActivated = true) + .scanProduct( + shouldCheckIsAlreadyActivated = true, + source = AnalyticsParam.ScreensSources.Upgrade, + ) .doOnSuccess { scanResponse -> checkIsWalletSuitableToBeUsedAsUpgrade(scanResponse = scanResponse) { delay(DELAY_SDK_DIALOG_CLOSE) 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 ef664fa1f0..5eafa28841 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 @@ -7,7 +7,6 @@ import com.tangem.common.ui.notifications.NotificationId import com.tangem.common.ui.userwallet.handle 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.Basic.ButtonSupport import com.tangem.core.analytics.models.event.AssetsDiscoveryAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped @@ -174,7 +173,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } override fun onGenerateMissedAddressesClick(missedAddressCurrencies: List) { - analyticsEventHandler.send(Basic.CardWasScanned(AnalyticsParam.ScreensSources.Main)) analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped()) modelScope.launch { @@ -199,7 +197,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( userWalletsListRepository.unlockAllWallets() .onLeft { val selectedUserWalletId = stateHolder.getSelectedWalletId() - nonBiometricUnlockWalletUseCase(selectedUserWalletId) + nonBiometricUnlockWalletUseCase(selectedUserWalletId, AnalyticsParam.ScreensSources.Main) .onLeft { error -> error.handle( onAlreadyUnlocked = {}, 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 c40e38cd57..87cbad4063 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 @@ -205,8 +205,11 @@ internal class WelcomeModel @Inject constructor( ).onLeft { error -> if (error is SaveWalletError.WalletAlreadySaved) { userWalletsListRepository.unlock( - userWallet.walletId, - unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), + userWalletId = userWallet.walletId, + unlockMethod = UserWalletsListRepository.UnlockMethod.Scan( + scanResponse = scanResponse, + source = AnalyticsParam.ScreensSources.SignIn, + ), ).onRight { userWalletsListRepository.select(userWallet.walletId) router.replaceAll(AppRoute.Wallet) @@ -271,7 +274,7 @@ internal class WelcomeModel @Inject constructor( } private suspend fun nonBiometricUnlockWallet(userWalletId: UserWalletId) { - nonBiometricUnlockWalletUseCase(userWalletId) + nonBiometricUnlockWalletUseCase(userWalletId, AnalyticsParam.ScreensSources.SignIn) .onRight { routedOut = true userWalletsListRepository.select(userWalletId) diff --git a/libs/tangem-sdk-api/build.gradle.kts b/libs/tangem-sdk-api/build.gradle.kts index 1a6d4da84d..813c8f51e6 100644 --- a/libs/tangem-sdk-api/build.gradle.kts +++ b/libs/tangem-sdk-api/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.visa.models) + api(projects.core.analytics.models) implementation(projects.core.configToggles) implementation(projects.core.res) diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt index a7697062bd..9ea0e0b15a 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt @@ -12,6 +12,7 @@ import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.UserCodeRequestPolicy import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.services.secure.SecureStorage +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO @@ -57,6 +58,7 @@ interface TangemSdkManager { messageRes: Int? = null, allowsRequestAccessCodeFromRepository: Boolean = false, shouldCheckIsAlreadyActivated: Boolean, + source: AnalyticsParam.ScreensSources, ): CompletionResult suspend fun createProductWallet(