Updated on 2026-08-14
This commit is contained in:
commit
5d853ea8b8
55 changed files with 951 additions and 199 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) } }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
|||
fun tokenWithTitleAndAddress(tokenTitle: String): KNode {
|
||||
return lazyList.childWith<LazyListItemNode> {
|
||||
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||
hasAnyDescendant(withText(tokenTitle))
|
||||
hasText(tokenTitle)
|
||||
useUnmergedTree = true
|
||||
}.child<KNode> {
|
||||
hasTestTag(TokenElementsTestTags.TOKEN_FIAT_AMOUNT)
|
||||
|
|
@ -256,7 +256,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
|||
fun tokenWithCustomDerivationIcon(tokenTitle: String): KNode {
|
||||
return lazyList.childWith<LazyListItemNode> {
|
||||
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||
hasAnyDescendant(withText(tokenTitle))
|
||||
hasText(tokenTitle)
|
||||
useUnmergedTree = true
|
||||
}.child<KNode> {
|
||||
hasTestTag(TokenElementsTestTags.TOKEN_CUSTOM_DERIVATION_ICON)
|
||||
|
|
@ -297,7 +297,7 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
|||
fun tokenWithTitleAndPosition(tokenTitle: String, index: Int): KNode {
|
||||
return lazyList.childWith<LazyListItemNode> {
|
||||
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||
hasAnyDescendant(withText(tokenTitle))
|
||||
hasText(tokenTitle)
|
||||
hasLazyListItemPosition(index)
|
||||
useUnmergedTree = true
|
||||
}.child<KNode> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -306,6 +306,26 @@
|
|||
android:host="news"
|
||||
android:scheme="tangem" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="earn"
|
||||
android:scheme="tangem" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="yield"
|
||||
android:scheme="tangem" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<ScanResponse> {
|
||||
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<ScanResponse>) {
|
||||
if (result is CompletionResult.Failure) {
|
||||
(result.error as? TangemSdkError)?.let { error ->
|
||||
Analytics.sendErrorEvent(TangemSdkErrorEvent(error))
|
||||
private fun sendAnalytics(result: CompletionResult<ScanResponse>, 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(
|
||||
|
|
|
|||
|
|
@ -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<ScanResponse> {
|
||||
if (!MockProvider.isPreset) {
|
||||
val activity = foregroundActivityObserver.foregroundActivity
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<EarnDeepLinkHandler.Factory>(relaxed = true) {
|
||||
every { create(any()) } returns mockk()
|
||||
}
|
||||
|
||||
private val yieldDeepLinkFactory = mockk<YieldDeepLinkHandler.Factory>(relaxed = true) {
|
||||
every { create(any(), any()) } returns mockk()
|
||||
}
|
||||
|
||||
private val marketsTokenExchangesDeepLinkFactory =
|
||||
mockk<MarketsTokenExchangesDeepLinkHandler.Factory>(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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
},
|
||||
{
|
||||
"name": "ASSETS_DISCOVERY_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.38"
|
||||
},
|
||||
{
|
||||
"name": "SOLANA_TX_HISTORY_ENABLED",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -71,15 +71,11 @@ internal class MockAwareOnboardingRepository @Inject constructor(
|
|||
override suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean> =
|
||||
real.hasTangemPayInWallet(userWalletId)
|
||||
|
||||
override suspend fun checkCustomerEligibility(): List<TangemPayEligibilityType> {
|
||||
if (isMockMode) return listOf(TangemPayEligibilityType.DETAILS)
|
||||
return real.checkCustomerEligibility()
|
||||
}
|
||||
override suspend fun checkCustomerEligibility(): List<TangemPayEligibilityType> =
|
||||
real.checkCustomerEligibility()
|
||||
|
||||
override suspend fun getCustomerEligibility(): List<TangemPayEligibilityType> {
|
||||
if (isMockMode) return listOf(TangemPayEligibilityType.DETAILS)
|
||||
return real.getCustomerEligibility()
|
||||
}
|
||||
override suspend fun getCustomerEligibility(): List<TangemPayEligibilityType> =
|
||||
real.getCustomerEligibility()
|
||||
|
||||
override fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? =
|
||||
real.getSavedCustomerInfo(userWalletId)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UnlockWalletError, Unit> = either {
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
source: AnalyticsParam.ScreensSources,
|
||||
): Either<UnlockWalletError, Unit> = 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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<UnlockWalletError, Unit> = either {
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
source: AnalyticsParam.ScreensSources,
|
||||
): Either<UnlockWalletError, Unit> = either {
|
||||
userWalletsListRepository.unlock(userWalletId, UserWalletsListRepository.UnlockMethod.Biometric)
|
||||
.mapLeft { error ->
|
||||
when (error) {
|
||||
UnlockWalletError.AlreadyUnlocked -> Unit
|
||||
else -> nonBiometricUnlockWalletUseCase(userWalletId).bind()
|
||||
else -> nonBiometricUnlockWalletUseCase(userWalletId, source).bind()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()) }
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.features.feed.entry.deeplink
|
||||
|
||||
interface EarnDeepLinkHandler {
|
||||
|
||||
interface Factory {
|
||||
fun create(queryParams: Map<String, String>): EarnDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, String>): YieldDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -112,4 +112,5 @@ dependencies {
|
|||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(deps.test.coroutine)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<String, String>,
|
||||
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<String, String>): DefaultEarnDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, String>,
|
||||
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<String, String>,
|
||||
): DefaultYieldDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<EarnFilter> {
|
||||
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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UserWallet> { 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<UserWallet> { 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<AppRoute.YieldSupplyEntry> {
|
||||
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<UserWallet> { 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<UserWallet> { 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<Network>(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<String, String>) {
|
||||
DefaultYieldDeepLinkHandler(
|
||||
scope = scope,
|
||||
queryParams = queryParams,
|
||||
appRouter = appRouter,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
yieldSupplyGetAvailabilityUseCase = yieldSupplyGetAvailabilityUseCase,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<CryptoCurrency>) {
|
||||
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 = {},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ScanResponse>
|
||||
|
||||
suspend fun createProductWallet(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue