Updated on 2026-08-14
This commit is contained in:
commit
63832bf7e7
56 changed files with 1540 additions and 270 deletions
|
|
@ -34,6 +34,7 @@
|
|||
<application
|
||||
android:name="com.tangem.tap.TangemHiltApplication"
|
||||
android:allowBackup="false"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="false"
|
||||
android:hardwareAccelerated="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
|
|
@ -43,7 +44,6 @@
|
|||
android:resizeableActivity="@bool/resizeable_activity"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
tools:ignore="GoogleAppIndexingWarning"
|
||||
tools:replace="android:allowBackup, android:fullBackupContent, android:label">
|
||||
|
||||
|
|
@ -208,6 +208,39 @@
|
|||
android:host="token_chart"
|
||||
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="buy"
|
||||
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="sell"
|
||||
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="swap"
|
||||
android:scheme="tangem" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Disable android.startup completely. Used for Worker according doc -->
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ package com.tangem.tap.routing.utils
|
|||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.feature.usedesk.api.UsedeskComponent
|
||||
import com.tangem.feature.qrscanning.QrScanningComponent
|
||||
import com.tangem.feature.referral.api.ReferralComponent
|
||||
import com.tangem.feature.stories.api.StoriesComponent
|
||||
import com.tangem.feature.usedesk.api.UsedeskComponent
|
||||
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
|
||||
import com.tangem.features.details.component.DetailsComponent
|
||||
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
|
||||
|
|
@ -49,7 +49,7 @@ internal class ChildFactory @Inject constructor(
|
|||
private val disclaimerComponentFactory: DisclaimerComponent.Factory,
|
||||
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
|
||||
private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
|
||||
private val marketsTokenListComponentFactory: MarketsTokenListComponent.Factory,
|
||||
private val marketsTokenListComponentFactory: MarketsTokenListComponent.FactoryScreen,
|
||||
private val onrampComponentFactory: OnrampComponent.Factory,
|
||||
private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory,
|
||||
private val buyCryptoComponentFactory: BuyCryptoComponent.Factory,
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import android.net.Uri
|
|||
import com.tangem.common.routing.AppRoute
|
||||
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.markets.deeplink.MarketsDeepLinkHandler
|
||||
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
|
||||
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
|
||||
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
|
||||
import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler
|
||||
import com.tangem.features.onramp.deeplink.*
|
||||
import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler
|
||||
import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler
|
||||
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
|
||||
import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler
|
||||
|
|
@ -21,6 +21,7 @@ import dagger.hilt.android.scopes.ActivityScoped
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import timber.log.Timber
|
||||
|
|
@ -29,9 +30,10 @@ import javax.inject.Inject
|
|||
@Suppress("LongParameterList")
|
||||
@ActivityScoped
|
||||
internal class DeepLinkFactory @Inject constructor(
|
||||
private val cardSdkProvider: CardSdkProvider,
|
||||
private val onrampDeepLink: OnrampDeepLinkHandler.Factory,
|
||||
private val sellDeepLink: SellDeepLinkHandler.Factory,
|
||||
private val buyDeepLink: BuyDeepLinkHandler.Factory,
|
||||
private val sellRedirectDeepLink: SellRedirectDeepLinkHandler.Factory,
|
||||
private val buyRedirectDeepLink: BuyRedirectDeepLinkHandler.Factory,
|
||||
private val referralDeepLink: ReferralDeepLinkHandler.Factory,
|
||||
private val walletConnectDeepLink: WalletConnectDeepLinkHandler.Factory,
|
||||
private val walletDeepLink: WalletDeepLinkHandler.Factory,
|
||||
|
|
@ -39,6 +41,9 @@ internal class DeepLinkFactory @Inject constructor(
|
|||
private val stakingDeepLink: StakingDeepLinkHandler.Factory,
|
||||
private val marketsDeepLink: MarketsDeepLinkHandler.Factory,
|
||||
private val marketsTokenDetailDeepLink: MarketsTokenDetailDeepLinkHandler.Factory,
|
||||
private val buyDeepLink: BuyDeepLinkHandler.Factory,
|
||||
private val sellDeepLink: SellDeepLinkHandler.Factory,
|
||||
private val swapDeepLink: SwapDeepLinkHandler.Factory,
|
||||
) {
|
||||
private val permittedAppRoute = MutableStateFlow(false)
|
||||
|
||||
|
|
@ -55,15 +60,19 @@ internal class DeepLinkFactory @Inject constructor(
|
|||
|- Received URI: $deeplinkUri
|
||||
""".trimIndent(),
|
||||
)
|
||||
permittedAppRoute
|
||||
.transformLatest<Boolean, Unit> { isPermitted ->
|
||||
if (isPermitted) {
|
||||
lastDeepLink?.let {
|
||||
launchDeepLink(it, coroutineScope, isFromOnNewIntent)
|
||||
}
|
||||
lastDeepLink = null
|
||||
combine(
|
||||
permittedAppRoute,
|
||||
cardSdkProvider.sdk.uiVisibility(),
|
||||
) { isRoutePermitted, isCardSdkVisible ->
|
||||
isRoutePermitted to isCardSdkVisible
|
||||
}.transformLatest<Pair<Boolean, Boolean>, Unit> { (isRoutePermitted, isCardSdkVisible) ->
|
||||
if (isRoutePermitted && !isCardSdkVisible) {
|
||||
lastDeepLink?.let {
|
||||
launchDeepLink(it, coroutineScope, isFromOnNewIntent)
|
||||
}
|
||||
lastDeepLink = null
|
||||
}
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
.saveIn(deepLinkHandlerJobHolder)
|
||||
}
|
||||
|
|
@ -103,8 +112,8 @@ internal class DeepLinkFactory @Inject constructor(
|
|||
val queryParams = getQueryParams(deeplinkUri)
|
||||
when (deeplinkUri.host) {
|
||||
DeepLinkRoute.Onramp.host -> onrampDeepLink.create(coroutineScope, queryParams)
|
||||
DeepLinkRoute.Sell.host -> sellDeepLink.create(coroutineScope, queryParams)
|
||||
DeepLinkRoute.Buy.host -> buyDeepLink.create(coroutineScope)
|
||||
DeepLinkRoute.SellRedirect.host -> sellRedirectDeepLink.create(coroutineScope, queryParams)
|
||||
DeepLinkRoute.BuyRedirect.host -> buyRedirectDeepLink.create(coroutineScope)
|
||||
DeepLinkRoute.Referral.host -> referralDeepLink.create()
|
||||
DeepLinkRoute.Wallet.host -> walletDeepLink.create()
|
||||
DeepLinkRoute.TokenDetails.host -> tokenDetailsDeepLink.create(
|
||||
|
|
@ -115,6 +124,9 @@ internal class DeepLinkFactory @Inject constructor(
|
|||
DeepLinkRoute.Staking.host -> stakingDeepLink.create(coroutineScope, queryParams)
|
||||
DeepLinkRoute.Markets.host -> marketsDeepLink.create()
|
||||
DeepLinkRoute.MarketTokenDetail.host -> marketsTokenDetailDeepLink.create(coroutineScope, queryParams)
|
||||
DeepLinkRoute.Buy.host -> buyDeepLink.create()
|
||||
DeepLinkRoute.Sell.host -> sellDeepLink.create()
|
||||
DeepLinkRoute.Swap.host -> swapDeepLink.create()
|
||||
else -> {
|
||||
Timber.i(
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ package com.tangem.tap.routing.utils
|
|||
|
||||
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.markets.deeplink.MarketsDeepLinkHandler
|
||||
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
|
||||
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
|
||||
import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler
|
||||
import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler
|
||||
import com.tangem.features.onramp.deeplink.*
|
||||
import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler
|
||||
import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler
|
||||
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
|
||||
import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler
|
||||
|
|
@ -18,6 +18,7 @@ import io.mockk.verify
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.*
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
|
|
@ -30,10 +31,10 @@ class DeepLinkFactoryTest {
|
|||
private val onrampDeepLinkFactory = mockk<OnrampDeepLinkHandler.Factory>(relaxed = true) {
|
||||
every { create(any(), any()) } returns mockk()
|
||||
}
|
||||
private val sellDeepLinkFactory = mockk<SellDeepLinkHandler.Factory>(relaxed = true) {
|
||||
private val sellRedirectDeepLinkFactory = mockk<SellRedirectDeepLinkHandler.Factory>(relaxed = true) {
|
||||
every { create(any(), any()) } returns mockk()
|
||||
}
|
||||
private val buyDeepLinkFactory = mockk<BuyDeepLinkHandler.Factory>(relaxed = true) {
|
||||
private val buyRedirectDeepLinkFactory = mockk<BuyRedirectDeepLinkHandler.Factory>(relaxed = true) {
|
||||
every { create(any()) } returns mockk()
|
||||
}
|
||||
private val referralDeepLinkFactory = mockk<ReferralDeepLinkHandler.Factory>(relaxed = true) {
|
||||
|
|
@ -57,7 +58,19 @@ class DeepLinkFactoryTest {
|
|||
private val marketsTokenDetailDeepLinkFactory = mockk<MarketsTokenDetailDeepLinkHandler.Factory>(relaxed = true) {
|
||||
every { create(any(), any()) } returns mockk()
|
||||
}
|
||||
private val sellDeepLinkFactory = mockk<SellDeepLinkHandler.Factory>(relaxed = true) {
|
||||
every { create() } returns mockk()
|
||||
}
|
||||
private val buyDeepLinkFactory = mockk<BuyDeepLinkHandler.Factory>(relaxed = true) {
|
||||
every { create() } returns mockk()
|
||||
}
|
||||
private val swapDeepLinkFactory = mockk<SwapDeepLinkHandler.Factory>(relaxed = true) {
|
||||
every { create() } returns mockk()
|
||||
}
|
||||
|
||||
private val cardSdkProvider = mockk<CardSdkProvider>(relaxed = true) {
|
||||
every { sdk.uiVisibility() } returns MutableStateFlow(false)
|
||||
}
|
||||
private val mockedUri = mockk<Uri>(relaxed = true)
|
||||
private val isFromOnNewIntent: Boolean = false
|
||||
|
||||
|
|
@ -65,16 +78,20 @@ class DeepLinkFactoryTest {
|
|||
private lateinit var testScope: TestScope
|
||||
|
||||
private val deepLinkFactory = DeepLinkFactory(
|
||||
onrampDeepLinkFactory,
|
||||
sellDeepLinkFactory,
|
||||
buyDeepLinkFactory,
|
||||
referralDeepLinkFactory,
|
||||
walletConnectDeepLinkFactory,
|
||||
walletDeepLinkFactory,
|
||||
tokenDetailsDeepLinkFactory,
|
||||
stakingDeepLinkFactory,
|
||||
marketsDeepLinkFactory,
|
||||
marketsTokenDetailDeepLinkFactory,
|
||||
cardSdkProvider = cardSdkProvider,
|
||||
onrampDeepLink = onrampDeepLinkFactory,
|
||||
sellRedirectDeepLink = sellRedirectDeepLinkFactory,
|
||||
buyRedirectDeepLink = buyRedirectDeepLinkFactory,
|
||||
referralDeepLink = referralDeepLinkFactory,
|
||||
walletConnectDeepLink = walletConnectDeepLinkFactory,
|
||||
walletDeepLink = walletDeepLinkFactory,
|
||||
tokenDetailsDeepLink = tokenDetailsDeepLinkFactory,
|
||||
stakingDeepLink = stakingDeepLinkFactory,
|
||||
marketsDeepLink = marketsDeepLinkFactory,
|
||||
marketsTokenDetailDeepLink = marketsTokenDetailDeepLinkFactory,
|
||||
buyDeepLink = buyDeepLinkFactory,
|
||||
sellDeepLink = sellDeepLinkFactory,
|
||||
swapDeepLink = swapDeepLinkFactory,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
|
@ -140,6 +157,24 @@ class DeepLinkFactoryTest {
|
|||
verify(inverse = true) { onrampDeepLinkFactory.create(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleDeeplink does not launch when card scan visible`() = runTest {
|
||||
every { mockedUri.scheme } returns "tangem"
|
||||
every { mockedUri.host } returns "onramp"
|
||||
every { mockedUri.query } returns "param=value"
|
||||
every { mockedUri.queryParameterNames } returns setOf("param")
|
||||
every { mockedUri.getQueryParameter("param") } returns "value"
|
||||
every { cardSdkProvider.sdk.uiVisibility() } returns MutableStateFlow(true)
|
||||
|
||||
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
|
||||
deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet)
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
// Verify no handler was called
|
||||
verify(inverse = true) { onrampDeepLinkFactory.create(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `launchDeepLink handles tangem scheme correctly`() = runTest {
|
||||
every { mockedUri.scheme } returns "tangem"
|
||||
|
|
@ -184,8 +219,8 @@ class DeepLinkFactoryTest {
|
|||
|
||||
verify(inverse = true) {
|
||||
onrampDeepLinkFactory.create(any(), any())
|
||||
sellDeepLinkFactory.create(any(), any())
|
||||
buyDeepLinkFactory.create(any())
|
||||
sellRedirectDeepLinkFactory.create(any(), any())
|
||||
buyRedirectDeepLinkFactory.create(any())
|
||||
referralDeepLinkFactory.create()
|
||||
walletConnectDeepLinkFactory.create(any())
|
||||
walletDeepLinkFactory.create()
|
||||
|
|
@ -208,11 +243,11 @@ class DeepLinkFactoryTest {
|
|||
advanceUntilIdle()
|
||||
verify { onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) }
|
||||
|
||||
// Test Sell
|
||||
// Test Sell Redirect
|
||||
every { mockedUri.host } returns "redirect_sell"
|
||||
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
|
||||
advanceUntilIdle()
|
||||
verify { sellDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) }
|
||||
verify { sellRedirectDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) }
|
||||
|
||||
// Test Token Details
|
||||
every { mockedUri.host } returns "token"
|
||||
|
|
@ -242,11 +277,11 @@ class DeepLinkFactoryTest {
|
|||
every { mockedUri.queryParameterNames } returns emptySet()
|
||||
every { mockedUri.getQueryParameter(any()) } returns ""
|
||||
|
||||
// Test Buy
|
||||
// Test Buy Redirect
|
||||
every { mockedUri.host } returns "redirect"
|
||||
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
|
||||
advanceUntilIdle()
|
||||
verify { buyDeepLinkFactory.create(eq(testScope)) }
|
||||
verify { buyRedirectDeepLinkFactory.create(eq(testScope)) }
|
||||
|
||||
// Test Referral
|
||||
every { mockedUri.host } returns "referral"
|
||||
|
|
@ -265,6 +300,24 @@ class DeepLinkFactoryTest {
|
|||
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
|
||||
advanceUntilIdle()
|
||||
verify { marketsDeepLinkFactory.create() }
|
||||
|
||||
// Test Sell
|
||||
every { mockedUri.host } returns "sell"
|
||||
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
|
||||
advanceUntilIdle()
|
||||
verify { sellDeepLinkFactory.create() }
|
||||
|
||||
// Test Swap
|
||||
every { mockedUri.host } returns "swap"
|
||||
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
|
||||
advanceUntilIdle()
|
||||
verify { sellDeepLinkFactory.create() }
|
||||
|
||||
// Test Buy
|
||||
every { mockedUri.host } returns "buy"
|
||||
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
|
||||
advanceUntilIdle()
|
||||
verify { buyDeepLinkFactory.create() }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -279,12 +332,15 @@ class DeepLinkFactoryTest {
|
|||
|
||||
verify(inverse = true) {
|
||||
onrampDeepLinkFactory.create(any(), any())
|
||||
sellDeepLinkFactory.create(any(), any())
|
||||
buyDeepLinkFactory.create(any())
|
||||
sellRedirectDeepLinkFactory.create(any(), any())
|
||||
buyRedirectDeepLinkFactory.create(any())
|
||||
referralDeepLinkFactory.create()
|
||||
walletConnectDeepLinkFactory.create(any())
|
||||
walletDeepLinkFactory.create()
|
||||
tokenDetailsDeepLinkFactory.create(any(), any(), any())
|
||||
buyDeepLinkFactory.create()
|
||||
sellDeepLinkFactory.create()
|
||||
swapDeepLinkFactory.create()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,14 +8,22 @@ sealed class DeepLinkRoute {
|
|||
override val host: String = "onramp"
|
||||
}
|
||||
|
||||
data object Sell : DeepLinkRoute() {
|
||||
data object SellRedirect : DeepLinkRoute() {
|
||||
override val host: String = "redirect_sell"
|
||||
}
|
||||
|
||||
data object Buy : DeepLinkRoute() {
|
||||
data object Sell : DeepLinkRoute() {
|
||||
override val host: String = "sell"
|
||||
}
|
||||
|
||||
data object BuyRedirect : DeepLinkRoute() {
|
||||
override val host: String = "redirect"
|
||||
}
|
||||
|
||||
data object Buy : DeepLinkRoute() {
|
||||
override val host: String = "buy"
|
||||
}
|
||||
|
||||
data object Referral : DeepLinkRoute() {
|
||||
override val host: String = "referral"
|
||||
}
|
||||
|
|
@ -39,6 +47,10 @@ sealed class DeepLinkRoute {
|
|||
data object MarketTokenDetail : DeepLinkRoute() {
|
||||
override val host: String = "token_chart"
|
||||
}
|
||||
|
||||
data object Swap : DeepLinkRoute() {
|
||||
override val host: String = "swap"
|
||||
}
|
||||
}
|
||||
|
||||
enum class DeepLinkScheme(val scheme: String) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
package com.tangem.core.deeplink
|
||||
|
||||
object DeeplinkConst {
|
||||
@Deprecated("Used only for push notifications mapping, use `WALLET_ID_KEY` only")
|
||||
const val PAYLOAD_WALLET_ID_KEY = "user_wallet_id"
|
||||
const val TANGEM_SCHEME = "tangem"
|
||||
const val WALLET_ID_KEY = "walletId"
|
||||
const val WALLET_ID_KEY = "user_wallet_id"
|
||||
const val NETWORK_ID_KEY = "network_id"
|
||||
const val TYPE_KEY = "type"
|
||||
const val TOKEN_ID_KEY = "token_id"
|
||||
const val DERIVATION_PATH_KEY = "derivation_path"
|
||||
const val TRANSACTION_ID_KEY = "transaction_id"
|
||||
const val NAME_KEY = "name"
|
||||
}
|
||||
|
|
@ -3,9 +3,11 @@ package com.tangem.core.deeplink.converter
|
|||
import com.tangem.common.routing.DeepLinkRoute
|
||||
import com.tangem.common.routing.DeepLinkScheme
|
||||
import com.tangem.core.deeplink.DEEPLINK_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.NAME_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.NETWORK_ID_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.PAYLOAD_WALLET_ID_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.TRANSACTION_ID_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.TYPE_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.WALLET_ID_KEY
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
|
@ -25,21 +27,29 @@ object PayloadToDeeplinkConverter : Converter<Map<String, String>, String?> {
|
|||
val type = payload[TYPE_KEY] ?: return null
|
||||
val networkId = payload[NETWORK_ID_KEY] ?: return null
|
||||
val tokenId = payload[TOKEN_ID_KEY] ?: return null
|
||||
val walletId = payload[PAYLOAD_WALLET_ID_KEY] ?: return null
|
||||
val walletId = payload[WALLET_ID_KEY] ?: return null
|
||||
val derivationPath = payload[DERIVATION_PATH_KEY] ?: return null
|
||||
val transactionId = payload[TRANSACTION_ID_KEY]
|
||||
val name = payload[NAME_KEY]
|
||||
|
||||
return DeepLinkBuilder().setScheme(DeepLinkScheme.Tangem.scheme)
|
||||
.setAction(DeepLinkRoute.TokenDetails.host)
|
||||
.addQueryParam(NETWORK_ID_KEY, networkId)
|
||||
.addQueryParam(TOKEN_ID_KEY, tokenId)
|
||||
.addQueryParam(TYPE_KEY, type)
|
||||
.addQueryParam(WALLET_ID_KEY, walletId)
|
||||
.build()
|
||||
return DeepLinkBuilder().setScheme(DeepLinkScheme.Tangem.scheme).apply {
|
||||
setAction(DeepLinkRoute.TokenDetails.host)
|
||||
addQueryParam(NETWORK_ID_KEY, networkId)
|
||||
addQueryParam(TOKEN_ID_KEY, tokenId)
|
||||
addQueryParam(TYPE_KEY, type)
|
||||
addQueryParam(WALLET_ID_KEY, walletId)
|
||||
addQueryParam(DERIVATION_PATH_KEY, derivationPath)
|
||||
|
||||
transactionId?.let { addQueryParam(TRANSACTION_ID_KEY, it) }
|
||||
name?.let { addQueryParam(NAME_KEY, it) }
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun isTangemPushNotificationPayload(payload: Map<String, String>): Boolean {
|
||||
return payload.containsKey(TYPE_KEY) &&
|
||||
payload.containsKey(NETWORK_ID_KEY) &&
|
||||
payload.containsKey(TOKEN_ID_KEY) &&
|
||||
payload.containsKey(PAYLOAD_WALLET_ID_KEY)
|
||||
payload.containsKey(WALLET_ID_KEY) &&
|
||||
payload.containsKey(DERIVATION_PATH_KEY)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,11 @@ package com.tangem.core.deeplink.converter
|
|||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.deeplink.DEEPLINK_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.NETWORK_ID_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.PAYLOAD_WALLET_ID_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.TYPE_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.WALLET_ID_KEY
|
||||
import org.junit.Test
|
||||
|
||||
internal class PayloadToDeeplinkConverterTest {
|
||||
|
|
@ -14,7 +15,8 @@ internal class PayloadToDeeplinkConverterTest {
|
|||
fun `GIVEN payload with deeplink key WHEN convert THEN should return deeplink value`() {
|
||||
// GIVEN
|
||||
val payload = mapOf(
|
||||
DEEPLINK_KEY to "tangem://token-details?networkId=ethereum&tokenId=0x123&type=token&walletId=wallet123",
|
||||
DEEPLINK_KEY to "tangem://token-details?networkId=ethereum&tokenId=0x123&type=token&user_wallet_id=wallet123" +
|
||||
"&derivation_path=m'0'0'0",
|
||||
)
|
||||
|
||||
// WHEN
|
||||
|
|
@ -22,7 +24,7 @@ internal class PayloadToDeeplinkConverterTest {
|
|||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(
|
||||
"tangem://token-details?networkId=ethereum&tokenId=0x123&type=token&walletId=wallet123",
|
||||
"tangem://token-details?networkId=ethereum&tokenId=0x123&type=token&user_wallet_id=wallet123&derivation_path=m'0'0'0",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -33,7 +35,8 @@ internal class PayloadToDeeplinkConverterTest {
|
|||
TYPE_KEY to "token",
|
||||
NETWORK_ID_KEY to "ethereum",
|
||||
TOKEN_ID_KEY to "0x123",
|
||||
PAYLOAD_WALLET_ID_KEY to "wallet123",
|
||||
WALLET_ID_KEY to "wallet123",
|
||||
DERIVATION_PATH_KEY to "m'0'0'0",
|
||||
)
|
||||
|
||||
// WHEN
|
||||
|
|
@ -41,7 +44,7 @@ internal class PayloadToDeeplinkConverterTest {
|
|||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(
|
||||
"tangem://token?network_id=ethereum&token_id=0x123&type=token&walletId=wallet123",
|
||||
"tangem://token?network_id=ethereum&token_id=0x123&type=token&user_wallet_id=wallet123&derivation_path=m'0'0'0",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +54,8 @@ internal class PayloadToDeeplinkConverterTest {
|
|||
val payload = mapOf(
|
||||
NETWORK_ID_KEY to "ethereum",
|
||||
TOKEN_ID_KEY to "0x123",
|
||||
PAYLOAD_WALLET_ID_KEY to "wallet123",
|
||||
WALLET_ID_KEY to "wallet123",
|
||||
DERIVATION_PATH_KEY to "m'0'0'0",
|
||||
)
|
||||
|
||||
// WHEN
|
||||
|
|
@ -67,7 +71,8 @@ internal class PayloadToDeeplinkConverterTest {
|
|||
val payload = mapOf(
|
||||
TYPE_KEY to "token",
|
||||
TOKEN_ID_KEY to "0x123",
|
||||
PAYLOAD_WALLET_ID_KEY to "wallet123",
|
||||
WALLET_ID_KEY to "wallet123",
|
||||
DERIVATION_PATH_KEY to "m'0'0'0",
|
||||
)
|
||||
|
||||
// WHEN
|
||||
|
|
@ -83,7 +88,8 @@ internal class PayloadToDeeplinkConverterTest {
|
|||
val payload = mapOf(
|
||||
TYPE_KEY to "token",
|
||||
NETWORK_ID_KEY to "ethereum",
|
||||
PAYLOAD_WALLET_ID_KEY to "wallet123",
|
||||
WALLET_ID_KEY to "wallet123",
|
||||
DERIVATION_PATH_KEY to "m'0'0'0",
|
||||
)
|
||||
|
||||
// WHEN
|
||||
|
|
|
|||
|
|
@ -119,7 +119,9 @@ internal class DefaultNFTRepository @Inject constructor(
|
|||
userWalletId: UserWalletId,
|
||||
networks: List<Network>,
|
||||
): Flow<List<NFTCollections>> = combine(
|
||||
networks.map { observeCollectionsInternal(userWalletId, it) },
|
||||
networks
|
||||
.sortedBy { it.name }
|
||||
.map { observeCollectionsInternal(userWalletId, it) },
|
||||
) { it.asList() }
|
||||
|
||||
private suspend fun observeCollectionsInternal(
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ android {
|
|||
namespace = "com.tangem.data.staking"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Core modules */
|
||||
implementation(projects.core.datasource)
|
||||
|
|
@ -64,7 +68,8 @@ dependencies {
|
|||
// endregion
|
||||
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(tangemDeps.card.core)
|
||||
|
|
|
|||
|
|
@ -246,6 +246,47 @@ internal class DefaultStakingRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getStakingAvailabilitySync(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): StakingAvailability {
|
||||
if (!checkFeatureToggleEnabled(cryptoCurrency.network.id)) {
|
||||
return StakingAvailability.Unavailable
|
||||
}
|
||||
|
||||
if (checkForInvalidCardBatch(userWalletId, cryptoCurrency)) {
|
||||
return StakingAvailability.Unavailable
|
||||
}
|
||||
|
||||
val rawCurrencyId = cryptoCurrency.id.rawCurrencyId
|
||||
if (rawCurrencyId == null) {
|
||||
return StakingAvailability.Unavailable
|
||||
}
|
||||
|
||||
val isSupportedInMobileApp = getSupportedIntegrationId(cryptoCurrency.id).isNullOrEmpty().not()
|
||||
|
||||
val yields = getEnabledYieldsSync()
|
||||
if (yields.isEmpty()) {
|
||||
return StakingAvailability.TemporaryUnavailable
|
||||
}
|
||||
|
||||
val prefetchedYield = findPrefetchedYield(
|
||||
yields = yields,
|
||||
currencyId = rawCurrencyId,
|
||||
symbol = cryptoCurrency.symbol,
|
||||
)
|
||||
|
||||
return when {
|
||||
prefetchedYield != null && isSupportedInMobileApp -> {
|
||||
StakingAvailability.Available(prefetchedYield.id)
|
||||
}
|
||||
prefetchedYield == null && isSupportedInMobileApp -> {
|
||||
StakingAvailability.TemporaryUnavailable
|
||||
}
|
||||
else -> StakingAvailability.Unavailable
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkFeatureToggleEnabled(networkId: Network.ID): Boolean {
|
||||
return when (networkId.toBlockchain()) {
|
||||
Blockchain.TON -> stakingFeatureToggles.isTonStakingEnabled
|
||||
|
|
|
|||
|
|
@ -0,0 +1,210 @@
|
|||
package com.tangem.data.staking.multi
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.left
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensure
|
||||
import arrow.core.toOption
|
||||
import com.tangem.data.common.api.safeApiCall
|
||||
import com.tangem.data.staking.store.YieldsBalancesStore
|
||||
import com.tangem.data.staking.utils.StakingIdFactory
|
||||
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
|
||||
import com.tangem.datasource.local.token.StakingYieldsStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.core.utils.catchOn
|
||||
import com.tangem.domain.staking.model.StakingID
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.models.isMultiCurrency
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Default implementation of [MultiYieldBalanceFetcher]
|
||||
*
|
||||
* @property userWalletsStore user wallets store
|
||||
* @property stakingYieldsStore staking yields store
|
||||
* @property yieldsBalancesStore yields balances store
|
||||
* @property stakingIdFactory factory for creating StakingID
|
||||
* @property stakeKitApi stake kit API
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultMultiYieldBalanceFetcherV2 @Inject constructor(
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val stakingYieldsStore: StakingYieldsStore,
|
||||
private val yieldsBalancesStore: YieldsBalancesStore,
|
||||
private val stakingIdFactory: StakingIdFactory,
|
||||
private val stakeKitApi: StakeKitApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
suspend fun invoke(params: MultiYieldBalanceFetcher.Params): Either<Throwable, Unit> {
|
||||
checkIsSupportedByWalletOrElse(userWalletId = params.userWalletId) {
|
||||
return it.left()
|
||||
}
|
||||
|
||||
val stakingIds = getStakingIds(params).getOrElse {
|
||||
return it.left()
|
||||
}
|
||||
|
||||
return Either.catchOn(dispatchers.default) {
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = stakingIds)
|
||||
|
||||
val availableStakingIds = getAvailableStakingIds(
|
||||
userWalletId = params.userWalletId,
|
||||
stakingIds = stakingIds,
|
||||
)
|
||||
|
||||
fetch(params = params, stakingIds = availableStakingIds)
|
||||
}
|
||||
.onLeft {
|
||||
Timber.e(it, "Unable to fetch yield balances $params")
|
||||
|
||||
yieldsBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = stakingIds)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun checkIsSupportedByWalletOrElse(userWalletId: UserWalletId, ifNotSupported: (Throwable) -> Unit) {
|
||||
val maybeUserWallet = userWalletsStore.getSyncOrNull(key = userWalletId).toOption()
|
||||
|
||||
val isSupportedByWallet = maybeUserWallet.isSome(UserWallet::isMultiCurrency)
|
||||
|
||||
if (!isSupportedByWallet) {
|
||||
val exception = IllegalStateException("Wallet $userWalletId is not supported: $maybeUserWallet")
|
||||
Timber.e(exception)
|
||||
|
||||
ifNotSupported(exception)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getStakingIds(params: MultiYieldBalanceFetcher.Params) = either {
|
||||
val stakingIds = catch(
|
||||
block = {
|
||||
params.currencyIdWithNetworkMap.flatMapTo(hashSetOf()) { (currencyId, network) ->
|
||||
stakingIdFactory.create(
|
||||
userWalletId = params.userWalletId,
|
||||
currencyId = currencyId,
|
||||
network = network,
|
||||
)
|
||||
}
|
||||
},
|
||||
catch = ::raise,
|
||||
)
|
||||
|
||||
ensure(stakingIds.isNotEmpty()) {
|
||||
val exception = IllegalStateException("Unable to create staking ids for $params: list is empty")
|
||||
Timber.e(exception)
|
||||
|
||||
raise(exception)
|
||||
}
|
||||
|
||||
stakingIds
|
||||
}
|
||||
|
||||
private suspend fun getAvailableStakingIds(userWalletId: UserWalletId, stakingIds: Set<StakingID>): Set<StakingID> {
|
||||
val yieldIds = getYieldsIds(userWalletId = userWalletId)
|
||||
|
||||
// [true] -> available
|
||||
// [false] -> unavailable
|
||||
val groupedStakingIds = stakingIds.groupBy { stakingId ->
|
||||
yieldIds.any { it == stakingId.integrationId }
|
||||
}
|
||||
|
||||
val availableStakingIds = groupedStakingIds[true].orEmpty()
|
||||
val unavailableStakingIds = groupedStakingIds[false].orEmpty()
|
||||
|
||||
if (unavailableStakingIds.isNotEmpty()) {
|
||||
yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = unavailableStakingIds.toSet())
|
||||
}
|
||||
|
||||
return availableStakingIds.toSet().ifEmpty {
|
||||
val exception = IllegalStateException(
|
||||
"""
|
||||
No available yields to fetch yield balances:
|
||||
– userWalletId: $userWalletId
|
||||
– stakingIds: ${stakingIds.joinToString()}
|
||||
""".trimIndent(),
|
||||
)
|
||||
Timber.d(exception)
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getYieldsIds(userWalletId: UserWalletId): Set<String> {
|
||||
val yieldsIds = stakingYieldsStore.getSyncWithTimeout().orEmpty()
|
||||
.mapNotNullTo(destination = hashSetOf(), transform = YieldDTO::id)
|
||||
|
||||
if (yieldsIds.isEmpty()) {
|
||||
val exception = IllegalStateException("No enabled yields for $userWalletId")
|
||||
Timber.e(exception)
|
||||
|
||||
throw exception
|
||||
}
|
||||
|
||||
return yieldsIds
|
||||
}
|
||||
|
||||
suspend fun fetch(params: MultiYieldBalanceFetcher.Params, stakingIds: Set<StakingID>) {
|
||||
safeApiCall(
|
||||
call = {
|
||||
val requests = stakingIds.map(YieldBalanceRequestBodyFactory::create)
|
||||
|
||||
val yieldBalances = coroutineScope {
|
||||
requests
|
||||
.chunked(size = 16)
|
||||
.map {
|
||||
async(dispatchers.io) { stakeKitApi.getMultipleYieldBalances(it).bind() }
|
||||
}
|
||||
.awaitAll()
|
||||
.flatten()
|
||||
.toSet()
|
||||
}
|
||||
|
||||
yieldsBalancesStore.storeActual(userWalletId = params.userWalletId, values = yieldBalances)
|
||||
|
||||
if (!allResponsesReceived(requests, yieldBalances)) {
|
||||
val values = stakingIds.filter { stakingId ->
|
||||
yieldBalances.none {
|
||||
stakingId.integrationId == it.integrationId &&
|
||||
stakingId.address == it.addresses.address
|
||||
}
|
||||
}
|
||||
|
||||
yieldsBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = values.toSet())
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
Timber.e(it, "Unable to fetch yield balances $params")
|
||||
|
||||
yieldsBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = stakingIds)
|
||||
|
||||
throw it
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun allResponsesReceived(
|
||||
requests: List<YieldBalanceRequestBody>,
|
||||
yieldBalances: Set<YieldBalanceWrapperDTO>,
|
||||
): Boolean {
|
||||
return requests.all { request ->
|
||||
yieldBalances.any {
|
||||
request.integrationId == it.integrationId &&
|
||||
request.addresses.address == it.addresses.address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.data.staking.single
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Default implementation of [MultiYieldBalanceFetcher]
|
||||
*
|
||||
* @property multiYieldBalanceFetcher multi yield balance fetcher
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultSingleYieldBalanceFetcherV2 @Inject constructor(
|
||||
private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
|
||||
) {
|
||||
|
||||
suspend fun invoke(params: SingleYieldBalanceFetcher.Params): Either<Throwable, Unit> {
|
||||
return multiYieldBalanceFetcher(
|
||||
params = YieldBalanceFetcherParams.Multi(
|
||||
userWalletId = params.userWalletId,
|
||||
currencyIdWithNetworkMap = mapOf(
|
||||
params.currencyId to params.network,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,470 @@
|
|||
package com.tangem.data.staking.multi
|
||||
|
||||
import arrow.core.toOption
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
|
||||
import com.tangem.common.test.data.staking.MockYieldDTOFactory
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||
import com.tangem.data.staking.store.YieldsBalancesStore
|
||||
import com.tangem.data.staking.utils.StakingIdFactory
|
||||
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.local.token.StakingYieldsStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.staking.model.StakingID
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultMultiYieldBalanceFetcherV2Test {
|
||||
|
||||
private val userWalletsStore: UserWalletsStore = mockk()
|
||||
private val stakingYieldsStore: StakingYieldsStore = mockk()
|
||||
private val yieldsBalancesStore: YieldsBalancesStore = mockk()
|
||||
private val stakingIdFactory: StakingIdFactory = mockk()
|
||||
private val stakeKitApi: StakeKitApi = mockk()
|
||||
|
||||
private val fetcher = DefaultMultiYieldBalanceFetcherV2(
|
||||
userWalletsStore = userWalletsStore,
|
||||
stakingYieldsStore = stakingYieldsStore,
|
||||
yieldsBalancesStore = yieldsBalancesStore,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
stakeKitApi = stakeKitApi,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(userWalletsStore, stakingYieldsStore, yieldsBalancesStore, stakingIdFactory, stakeKitApi)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances successfully`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
|
||||
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId))
|
||||
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields
|
||||
|
||||
val requests = tonAndSolanaIds.map(YieldBalanceRequestBodyFactory::create).sortedBy { it.integrationId }
|
||||
val result = setOf(
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId),
|
||||
MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId),
|
||||
)
|
||||
coEvery { stakeKitApi.getMultipleYieldBalances(requests) } returns ApiResponse.Success(result)
|
||||
coEvery { yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result) } just Runs
|
||||
|
||||
// Actual
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
|
||||
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
stakeKitApi.getMultipleYieldBalances(requests)
|
||||
yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) { yieldsBalancesStore.storeError(any(), any()) }
|
||||
|
||||
Truth.assertThat(actual.isRight()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances successfully if one of stakingIds is unavailable`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
|
||||
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
val yields = listOf(MockYieldDTOFactory.create(tonId))
|
||||
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields
|
||||
|
||||
coEvery { yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(solanaId)) } just Runs
|
||||
|
||||
val requests = listOf(YieldBalanceRequestBodyFactory.create(tonId))
|
||||
val result = setOf(MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId))
|
||||
|
||||
coEvery { stakeKitApi.getMultipleYieldBalances(requests) } returns ApiResponse.Success(result)
|
||||
coEvery { yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result) } just Runs
|
||||
|
||||
// Actual
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
|
||||
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(solanaId))
|
||||
stakeKitApi.getMultipleYieldBalances(requests)
|
||||
yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result)
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isRight()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if user wallet is not supported`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
|
||||
val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false)
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
|
||||
// Actual
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify { userWalletsStore.getSyncOrNull(params.userWalletId) }
|
||||
|
||||
coVerify(inverse = true) {
|
||||
stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network)
|
||||
yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any())
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any())
|
||||
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
|
||||
yieldsBalancesStore.storeError(userWalletId = any(), stakingIds = any())
|
||||
}
|
||||
|
||||
val expected = IllegalStateException("Wallet ${params.userWalletId} is not supported: ${userWallet.toOption()}")
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if userWalletsStore returns null`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns null
|
||||
|
||||
// Actual
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify { userWalletsStore.getSyncOrNull(params.userWalletId) }
|
||||
|
||||
coVerify(inverse = true) {
|
||||
stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network)
|
||||
yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any())
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any())
|
||||
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
|
||||
yieldsBalancesStore.storeError(userWalletId = any(), stakingIds = any())
|
||||
}
|
||||
|
||||
val expected = IllegalStateException("Wallet ${params.userWalletId} is not supported: ${null.toOption()}")
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if stakingIdFactory returns empty list`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns emptySet()
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns emptySet()
|
||||
|
||||
// Actual
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
|
||||
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
yieldsBalancesStore.refresh(any(), any<Set<StakingID>>())
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
stakeKitApi.getMultipleYieldBalances(any())
|
||||
yieldsBalancesStore.storeActual(any(), any())
|
||||
yieldsBalancesStore.storeError(any(), any())
|
||||
}
|
||||
|
||||
val expected = IllegalStateException("Unable to create staking ids for $params: list is empty")
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns null`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
|
||||
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
|
||||
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns null
|
||||
coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
// Actual
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
|
||||
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
|
||||
yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
stakeKitApi.getMultipleYieldBalances(any())
|
||||
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
|
||||
}
|
||||
|
||||
val expected = IllegalStateException("No enabled yields for ${params.userWalletId}")
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns empty list`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
|
||||
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
|
||||
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns emptyList()
|
||||
coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
// Actual
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
|
||||
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
stakeKitApi.getMultipleYieldBalances(any())
|
||||
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
|
||||
}
|
||||
|
||||
val expected = IllegalStateException("No enabled yields for ${params.userWalletId}")
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if yields converting is failed`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
|
||||
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
val yields = listOf(
|
||||
MockYieldDTOFactory.create(tonId).copy(id = null),
|
||||
MockYieldDTOFactory.create(solanaId).copy(id = null),
|
||||
)
|
||||
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields
|
||||
coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
// Actual
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
|
||||
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
stakeKitApi.getMultipleYieldBalances(any())
|
||||
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
|
||||
}
|
||||
|
||||
val expected = IllegalStateException("No enabled yields for ${params.userWalletId}")
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if available yields does not contain ids from params`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
|
||||
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
val yields = listOf(MockYieldDTOFactory.create(StakingID(integrationId = "polygon", address = "0x1")))
|
||||
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields
|
||||
coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
// Actual
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
|
||||
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
stakeKitApi.getMultipleYieldBalances(any())
|
||||
yieldsBalancesStore.storeActual(userWalletId = any(), values = any())
|
||||
}
|
||||
|
||||
val expected = IllegalStateException(
|
||||
"""
|
||||
No available yields to fetch yield balances:
|
||||
– userWalletId: $userWalletId
|
||||
– stakingIds: ${setOf(solanaId, tonId).joinToString()}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yields balances failure if stakeKitApi getMultipleYieldBalances is failed`() = runTest {
|
||||
// Arrange
|
||||
val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network)
|
||||
|
||||
val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap)
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId)
|
||||
coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId)
|
||||
coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId))
|
||||
coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields
|
||||
|
||||
val requests = setOf(solanaId, tonId).map(YieldBalanceRequestBodyFactory::create)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException)
|
||||
as ApiResponse<Set<YieldBalanceWrapperDTO>>
|
||||
|
||||
coEvery { stakeKitApi.getMultipleYieldBalances(requests) } returns errorResponse
|
||||
coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs
|
||||
|
||||
// Actual
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
userWalletsStore.getSyncOrNull(params.userWalletId)
|
||||
stakingIdFactory.create(params.userWalletId, ton.id, ton.network)
|
||||
stakingIdFactory.create(params.userWalletId, solana.id, solana.network)
|
||||
yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds)
|
||||
stakingYieldsStore.getSyncWithTimeout()
|
||||
stakeKitApi.getMultipleYieldBalances(requests)
|
||||
yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = tonAndSolanaIds)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) { yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) }
|
||||
|
||||
val expected = ApiResponseError.NetworkException
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val userWalletId = UserWalletId("011")
|
||||
val userWallet = MockUserWalletFactory.create()
|
||||
|
||||
val mocks = MockCryptoCurrencyFactory()
|
||||
|
||||
val ton = mocks.createCoin(Blockchain.TON)
|
||||
val solana = mocks.createCoin(Blockchain.Solana)
|
||||
|
||||
val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId
|
||||
val solanaId = StakingID(
|
||||
integrationId = "solana-sol-native-multivalidator-staking",
|
||||
address = "0x1",
|
||||
)
|
||||
|
||||
val tonAndSolanaIds = setOf(tonId, solanaId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package com.tangem.data.staking.single
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultSingleYieldBalanceFetcherV2Test {
|
||||
|
||||
private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher = mockk()
|
||||
|
||||
private val fetcher = DefaultSingleYieldBalanceFetcherV2(
|
||||
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(multiYieldBalanceFetcher)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yield balance successfully`() = runTest {
|
||||
// Arrange
|
||||
val params = SingleYieldBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = ton.id,
|
||||
network = ton.network,
|
||||
)
|
||||
|
||||
val multiParams = YieldBalanceFetcherParams.Multi(
|
||||
userWalletId = userWalletId,
|
||||
currencyIdWithNetworkMap = mapOf(ton.id to ton.network),
|
||||
)
|
||||
|
||||
val multiResult = Unit.right()
|
||||
|
||||
coEvery { multiYieldBalanceFetcher(params = multiParams) } returns multiResult
|
||||
|
||||
// Act
|
||||
val actual = fetcher.invoke(params).isRight()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isTrue()
|
||||
|
||||
coVerify { multiYieldBalanceFetcher(params = multiParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch yield balance failure`() = runTest {
|
||||
// Arrange
|
||||
val params = SingleYieldBalanceFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
currencyId = ton.id,
|
||||
network = ton.network,
|
||||
)
|
||||
|
||||
val multiParams = YieldBalanceFetcherParams.Multi(
|
||||
userWalletId = userWalletId,
|
||||
currencyIdWithNetworkMap = mapOf(ton.id to ton.network),
|
||||
)
|
||||
|
||||
val multiResult = IllegalStateException().left()
|
||||
|
||||
coEvery { multiYieldBalanceFetcher(params = multiParams) } returns multiResult
|
||||
|
||||
// Act
|
||||
val actual = fetcher.invoke(params)
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEqualTo(multiResult)
|
||||
coVerify { multiYieldBalanceFetcher(params = multiParams) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val userWalletId = UserWalletId("011")
|
||||
val ton = MockCryptoCurrencyFactory().createCoin(Blockchain.TON)
|
||||
}
|
||||
}
|
||||
|
|
@ -502,7 +502,8 @@ internal class DefaultCurrenciesRepository(
|
|||
currencyRawId: CryptoCurrency.RawID,
|
||||
): Flow<Map<UserWallet, List<CryptoCurrency>>> {
|
||||
return userWalletsStore.userWallets.flatMapLatest { userWallets ->
|
||||
userWallets.forEach { fetchTokensIfCacheExpired(userWallet = it, refresh = false) }
|
||||
userWallets.filter { it.isMultiCurrency }
|
||||
.forEach { fetchTokensIfCacheExpired(userWallet = it, refresh = false) }
|
||||
|
||||
val userWalletsWithCurrencies = userWallets
|
||||
.filterNot(UserWallet::isLocked)
|
||||
|
|
|
|||
|
|
@ -32,4 +32,14 @@ class GetStakingAvailabilityUseCase(
|
|||
it.right()
|
||||
}.catch { emit(stakingErrorResolver.resolve(it).left()) }
|
||||
}
|
||||
|
||||
suspend fun invokeSync(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): Either<StakingError, StakingAvailability> = Either.catch {
|
||||
stakingRepository.getStakingAvailabilitySync(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
)
|
||||
}.mapLeft(stakingErrorResolver::resolve)
|
||||
}
|
||||
|
|
@ -1,11 +1,26 @@
|
|||
package com.tangem.domain.staking.multi
|
||||
|
||||
import com.tangem.domain.core.flow.FlowFetcher
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
/**
|
||||
* Fetcher of yields balances
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface MultiYieldBalanceFetcher : FlowFetcher<YieldBalanceFetcherParams.Multi>
|
||||
interface MultiYieldBalanceFetcher : FlowFetcher<YieldBalanceFetcherParams.Multi> {
|
||||
|
||||
/**
|
||||
* Params for fetching multiple yield balances
|
||||
*
|
||||
* @property userWalletId user wallet ID
|
||||
* @property currencyIdWithNetworkMap map of currency ID to network
|
||||
*/
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val currencyIdWithNetworkMap: Map<CryptoCurrency.ID, Network>,
|
||||
)
|
||||
}
|
||||
|
|
@ -37,6 +37,11 @@ interface StakingRepository {
|
|||
|
||||
fun getStakingAvailability(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Flow<StakingAvailability>
|
||||
|
||||
suspend fun getStakingAvailabilitySync(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): StakingAvailability
|
||||
|
||||
suspend fun getActions(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,28 @@
|
|||
package com.tangem.domain.staking.single
|
||||
|
||||
import com.tangem.domain.core.flow.FlowFetcher
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
/**
|
||||
* Fetcher of yield balance
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface SingleYieldBalanceFetcher : FlowFetcher<YieldBalanceFetcherParams.Single>
|
||||
interface SingleYieldBalanceFetcher : FlowFetcher<YieldBalanceFetcherParams.Single> {
|
||||
|
||||
/**
|
||||
* Params for fetching single yield balance
|
||||
*
|
||||
* @property userWalletId user wallet ID
|
||||
* @property currencyId currency ID
|
||||
* @property network network
|
||||
*/
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val currencyId: CryptoCurrency.ID,
|
||||
val network: Network,
|
||||
)
|
||||
}
|
||||
|
|
@ -48,6 +48,11 @@ class MockStakingRepository : StakingRepository {
|
|||
cryptoCurrency: CryptoCurrency,
|
||||
): Flow<StakingAvailability> = flowOf(StakingAvailability.Unavailable)
|
||||
|
||||
override suspend fun getStakingAvailabilitySync(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): StakingAvailability = StakingAvailability.Unavailable
|
||||
|
||||
override suspend fun getActions(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class DefaultUserWalletsSyncDelegate(
|
|||
raise(UpdateWalletError.NameAlreadyExists)
|
||||
}
|
||||
|
||||
return@withContext when (
|
||||
when (
|
||||
val result =
|
||||
userWalletsListManager.update(userWalletId) { it.copy(name = name) }
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@ import androidx.compose.runtime.Stable
|
|||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.markets.entry.BottomSheetState
|
||||
|
||||
@Stable
|
||||
|
|
@ -19,5 +22,13 @@ interface MarketsTokenListComponent : ComposableContentComponent {
|
|||
modifier: Modifier,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Unit, MarketsTokenListComponent>
|
||||
interface FactoryScreen : ComponentFactory<Unit, MarketsTokenListComponent>
|
||||
|
||||
interface FactoryBottomSheet {
|
||||
fun create(
|
||||
context: AppComponentContext,
|
||||
params: Unit,
|
||||
onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit)?,
|
||||
): MarketsTokenListComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@ dependencies {
|
|||
/* Project - API */
|
||||
api(projects.features.markets.api)
|
||||
api(projects.features.onramp.api)
|
||||
implementation(projects.core.navigation)
|
||||
|
||||
/* Data */
|
||||
implementation(projects.data.common)
|
||||
|
|
@ -38,6 +37,7 @@ dependencies {
|
|||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.notifications.models)
|
||||
|
||||
// FIXME [REDACTED_TASK_KEY]
|
||||
// Remove the "Buy" and "Sell" actions from the redux middleware.
|
||||
|
|
@ -73,6 +73,8 @@ dependencies {
|
|||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.deepLinks)
|
||||
|
||||
/* Common */
|
||||
implementation(projects.common.ui)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.markets.deeplink
|
|||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
|
||||
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
|
|
@ -17,14 +18,18 @@ import kotlinx.coroutines.launch
|
|||
import timber.log.Timber
|
||||
|
||||
internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject constructor(
|
||||
@Assisted scope: CoroutineScope,
|
||||
@Assisted queryParams: Map<String, String>,
|
||||
appRouter: AppRouter,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
|
||||
@Assisted private val scope: CoroutineScope,
|
||||
@Assisted private val queryParams: Map<String, String>,
|
||||
private val appRouter: AppRouter,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
|
||||
) : MarketsTokenDetailDeepLinkHandler {
|
||||
|
||||
init {
|
||||
handleDeepLink()
|
||||
}
|
||||
|
||||
private fun handleDeepLink() {
|
||||
val tokenId = queryParams[TOKEN_ID_KEY]
|
||||
|
||||
val rawTokenId = CryptoCurrency.RawID(tokenId.orEmpty())
|
||||
|
|
@ -36,7 +41,7 @@ internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject construc
|
|||
val tokenInfo = getTokenMarketInfoUseCase(
|
||||
appCurrency = appCurrency,
|
||||
tokenId = rawTokenId,
|
||||
tokenSymbol = TOKEN_SYMBOL_KEY,
|
||||
tokenSymbol = "", // used for analytics
|
||||
).getOrElse {
|
||||
Timber.e("Failed to get market token info")
|
||||
return@launch
|
||||
|
|
@ -71,9 +76,4 @@ internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject construc
|
|||
queryParams: Map<String, String>,
|
||||
): DefaultMarketsTokenDetailDeepLinkHandler
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TOKEN_ID_KEY = "token_id"
|
||||
const val TOKEN_SYMBOL_KEY = "token_symbol"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.Stable
|
|||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.arkivanov.decompose.ExperimentalDecomposeApi
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
|
|
@ -14,6 +15,9 @@ import com.arkivanov.decompose.value.Value
|
|||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.navigation.inner.InnerRouter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.entry.BottomSheetState
|
||||
import com.tangem.features.markets.entry.MarketsEntryComponent
|
||||
import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child
|
||||
|
|
@ -48,6 +52,7 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor(
|
|||
componentContext = factoryContext,
|
||||
router = innerRouter,
|
||||
),
|
||||
onTokenClick = ::marketsListTokenSelected,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -67,6 +72,23 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalDecomposeApi::class)
|
||||
private fun marketsListTokenSelected(token: TokenMarketParams, appCurrency: AppCurrency) {
|
||||
innerRouter.push(
|
||||
route = Child.TokenDetails(
|
||||
params = MarketsTokenDetailsComponent.Params(
|
||||
token = token,
|
||||
appCurrency = appCurrency,
|
||||
showPortfolio = true,
|
||||
analyticsParams = MarketsTokenDetailsComponent.AnalyticsParams(
|
||||
blockchain = null,
|
||||
source = "Market",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onChildBack() {
|
||||
if (stack.value.active.configuration !is Child.TokenList) {
|
||||
stackNavigation.popWhile { it != Child.TokenList }
|
||||
|
|
|
|||
|
|
@ -3,13 +3,15 @@ package com.tangem.features.markets.entry.impl
|
|||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
|
||||
import kotlinx.serialization.Serializable
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class MarketsEntryChildFactory @Inject constructor(
|
||||
private val tokenListComponentFactory: MarketsTokenListComponent.Factory,
|
||||
private val tokenListComponentFactory: MarketsTokenListComponent.FactoryBottomSheet,
|
||||
private val tokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
|
||||
) {
|
||||
|
||||
|
|
@ -26,7 +28,11 @@ internal class MarketsEntryChildFactory @Inject constructor(
|
|||
data class TokenDetails(val params: MarketsTokenDetailsComponent.Params) : Child
|
||||
}
|
||||
|
||||
fun createChild(child: Child, appComponentContext: AppComponentContext): Any {
|
||||
fun createChild(
|
||||
child: Child,
|
||||
appComponentContext: AppComponentContext,
|
||||
onTokenClick: (TokenMarketParams, AppCurrency) -> Unit,
|
||||
): Any {
|
||||
return when (child) {
|
||||
is Child.TokenDetails -> {
|
||||
tokenDetailsComponentFactory.create(
|
||||
|
|
@ -38,6 +44,7 @@ internal class MarketsEntryChildFactory @Inject constructor(
|
|||
tokenListComponentFactory.create(
|
||||
context = appComponentContext,
|
||||
params = Unit,
|
||||
onTokenClick = onTokenClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ internal fun EntryBottomSheetContent(
|
|||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
MarketsEntryChildFactory.Child.TokenList -> {
|
||||
is MarketsEntryChildFactory.Child.TokenList -> {
|
||||
(it.instance as MarketsTokenListComponent).BottomSheetContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
|
|
@ -84,7 +84,7 @@ private fun BackgroundColorEffects(
|
|||
animationSpec = tween(durationMillis = 500),
|
||||
)
|
||||
}
|
||||
MarketsEntryChildFactory.Child.TokenList -> {
|
||||
is MarketsEntryChildFactory.Child.TokenList -> {
|
||||
backgroundColor.animateTo(
|
||||
primary,
|
||||
animationSpec = tween(durationMillis = 500),
|
||||
|
|
@ -119,7 +119,7 @@ private fun BackgroundColorEffects(
|
|||
is MarketsEntryChildFactory.Child.TokenDetails -> {
|
||||
backgroundColor.snapTo(tertiary)
|
||||
}
|
||||
MarketsEntryChildFactory.Child.TokenList -> {
|
||||
is MarketsEntryChildFactory.Child.TokenList -> {
|
||||
backgroundColor.snapTo(primary)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.markets.toSerializableParam
|
||||
import com.tangem.features.markets.entry.BottomSheetState
|
||||
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
|
||||
|
|
@ -31,9 +33,10 @@ import kotlinx.coroutines.flow.launchIn
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
class DefaultMarketsTokenListComponent @AssistedInject constructor(
|
||||
internal class DefaultMarketsTokenListComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: Unit,
|
||||
@Assisted onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit)? = null,
|
||||
) : AppComponentContext by appComponentContext, MarketsTokenListComponent {
|
||||
|
||||
private val model: MarketsListModel = getOrCreateModel()
|
||||
|
|
@ -41,17 +44,23 @@ class DefaultMarketsTokenListComponent @AssistedInject constructor(
|
|||
init {
|
||||
model.tokenSelected
|
||||
.onEach { (token, appCurrency) ->
|
||||
router.push(
|
||||
AppRoute.MarketsTokenDetails(
|
||||
token = token.toSerializableParam(),
|
||||
appCurrency = appCurrency,
|
||||
showPortfolio = true,
|
||||
analyticsParams = AnalyticsParams(
|
||||
blockchain = null,
|
||||
source = "Market",
|
||||
// If specific routing call back is provided, invoke it (for example inner routing in Bottom Sheet)
|
||||
// Otherwise navigate using app routing
|
||||
if (onTokenClick != null) {
|
||||
onTokenClick(token.toSerializableParam(), appCurrency)
|
||||
} else {
|
||||
router.push(
|
||||
AppRoute.MarketsTokenDetails(
|
||||
token = token.toSerializableParam(),
|
||||
appCurrency = appCurrency,
|
||||
showPortfolio = true,
|
||||
analyticsParams = AnalyticsParams(
|
||||
blockchain = null,
|
||||
source = "Market",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.launchIn(componentScope)
|
||||
}
|
||||
|
|
@ -112,7 +121,11 @@ class DefaultMarketsTokenListComponent @AssistedInject constructor(
|
|||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MarketsTokenListComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: Unit): DefaultMarketsTokenListComponent
|
||||
interface FactoryBottomSheet : MarketsTokenListComponent.FactoryBottomSheet {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: Unit,
|
||||
onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit)?,
|
||||
): DefaultMarketsTokenListComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
package com.tangem.features.markets.tokenlist.impl.di
|
||||
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
|
||||
import com.tangem.features.markets.tokenlist.impl.DefaultMarketsTokenListComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -14,7 +16,24 @@ internal interface ComponentModule {
|
|||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindMarketsTokenListComponent(
|
||||
factory: DefaultMarketsTokenListComponent.Factory,
|
||||
): MarketsTokenListComponent.Factory
|
||||
fun bindMarketsTokenListBottomSheetComponent(
|
||||
factory: DefaultMarketsTokenListComponent.FactoryBottomSheet,
|
||||
): MarketsTokenListComponent.FactoryBottomSheet
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal class ComponentProvideModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesMarketsTokenListScreenComponent(
|
||||
factory: DefaultMarketsTokenListComponent.FactoryBottomSheet,
|
||||
): MarketsTokenListComponent.FactoryScreen {
|
||||
return object : MarketsTokenListComponent.FactoryScreen {
|
||||
override fun create(context: AppComponentContext, params: Unit): MarketsTokenListComponent {
|
||||
return factory.create(context, params, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.nft.models.NFTAsset
|
||||
import com.tangem.domain.nft.models.NFTCollection
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.nft.collections.model.NFTCollectionsModel
|
||||
import com.tangem.features.nft.collections.ui.NFTCollections
|
||||
|
|
@ -31,7 +32,7 @@ internal class NFTCollectionsComponent @AssistedInject constructor(
|
|||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val onBackClick: () -> Unit,
|
||||
val onAssetClick: (asset: NFTAsset, collectionName: String) -> Unit,
|
||||
val onAssetClick: (asset: NFTAsset, collection: NFTCollection) -> Unit,
|
||||
val onReceiveClick: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ internal class UpdateDataStateTransformer(
|
|||
private val onRetryClick: () -> Unit,
|
||||
private val onExpandCollectionClick: (NFTCollection) -> Unit,
|
||||
private val onRetryAssetsClick: (NFTCollection) -> Unit,
|
||||
private val onAssetClick: (NFTAsset, String) -> Unit,
|
||||
private val onAssetClick: (NFTAsset, NFTCollection) -> Unit,
|
||||
private val initialSearchBarFactory: () -> SearchBarUM,
|
||||
private val collectionIdProvider: NFTCollection.() -> String,
|
||||
) : Transformer<NFTCollectionsStateUM> {
|
||||
|
|
@ -118,12 +118,12 @@ internal class UpdateDataStateTransformer(
|
|||
is NFTCollection.Assets.Value -> NFTCollectionAssetsListUM.Content(
|
||||
items = assets
|
||||
.items
|
||||
.map { it.transform(name.orEmpty()) }
|
||||
.map { it.transform(this) }
|
||||
.toPersistentList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun NFTAsset.transform(collectionName: String): NFTCollectionAssetUM {
|
||||
private fun NFTAsset.transform(collection: NFTCollection): NFTCollectionAssetUM {
|
||||
return NFTCollectionAssetUM(
|
||||
id = id.toString(),
|
||||
name = name ?: DASH_SIGN,
|
||||
|
|
@ -144,7 +144,7 @@ internal class UpdateDataStateTransformer(
|
|||
)
|
||||
},
|
||||
onItemClick = {
|
||||
onAssetClick(this, collectionName)
|
||||
onAssetClick(this, collection)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,8 +79,8 @@ internal class NFTCollectionsModel @Inject constructor(
|
|||
onRetryClick = ::onRefresh,
|
||||
onExpandCollectionClick = ::onExpandCollectionClick,
|
||||
onRetryAssetsClick = ::onRetryAssetsClick,
|
||||
onAssetClick = { asset, collectionName ->
|
||||
params.onAssetClick(asset, collectionName)
|
||||
onAssetClick = { asset, collection ->
|
||||
params.onAssetClick(asset, collection)
|
||||
},
|
||||
initialSearchBarFactory = ::getInitialSearchBar,
|
||||
collectionIdProvider = collectionIdProvider,
|
||||
|
|
|
|||
|
|
@ -112,12 +112,12 @@ internal class DefaultNFTComponent @AssistedInject constructor(
|
|||
),
|
||||
)
|
||||
},
|
||||
onAssetClick = { asset, collectionName ->
|
||||
onAssetClick = { asset, collection ->
|
||||
innerRouter.push(
|
||||
NFTRoute.Details(
|
||||
userWalletId = route.userWalletId,
|
||||
nftAsset = asset,
|
||||
collectionName = collectionName,
|
||||
collection = collection,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
@ -144,7 +144,7 @@ internal class DefaultNFTComponent @AssistedInject constructor(
|
|||
params = NFTDetailsComponent.Params(
|
||||
userWalletId = route.userWalletId,
|
||||
nftAsset = route.nftAsset,
|
||||
nftCollectionName = route.collectionName,
|
||||
nftCollection = route.collection,
|
||||
onBackClick = ::onChildBack,
|
||||
onAllTraitsClick = {
|
||||
innerRouter.push(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.nft.common
|
|||
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.domain.nft.models.NFTAsset
|
||||
import com.tangem.domain.nft.models.NFTCollection
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -21,7 +22,7 @@ internal sealed class NFTRoute : Route {
|
|||
data class Details(
|
||||
val userWalletId: UserWalletId,
|
||||
val nftAsset: NFTAsset,
|
||||
val collectionName: String,
|
||||
val collection: NFTCollection,
|
||||
) : NFTRoute()
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.core.decompose.model.getOrCreateModel
|
|||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.nft.models.NFTAsset
|
||||
import com.tangem.domain.nft.models.NFTCollection
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.nft.details.entity.NFTDetailsBottomSheetConfig
|
||||
import com.tangem.features.nft.details.info.NFTDetailsInfoComponent
|
||||
|
|
@ -65,7 +66,7 @@ internal class NFTDetailsComponent @AssistedInject constructor(
|
|||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val nftAsset: NFTAsset,
|
||||
val nftCollectionName: String,
|
||||
val nftCollection: NFTCollection,
|
||||
val onBackClick: () -> Unit,
|
||||
val onAllTraitsClick: () -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat
|
|||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.nft.models.NFTAsset
|
||||
import com.tangem.domain.nft.models.NFTCollection
|
||||
import com.tangem.domain.nft.models.NFTSalePrice
|
||||
import com.tangem.features.nft.details.entity.NFTAssetUM
|
||||
import com.tangem.features.nft.details.entity.NFTDetailsUM
|
||||
|
|
@ -29,8 +30,8 @@ internal class NFTDetailsUMFactory(
|
|||
private val onInfoBlockClick: (title: TextReference, text: TextReference) -> Unit,
|
||||
) {
|
||||
|
||||
fun getInitialState(nftAsset: NFTAsset): NFTDetailsUM = NFTDetailsUM(
|
||||
nftAsset = nftAsset.transform(),
|
||||
fun getInitialState(nftAsset: NFTAsset, nftCollection: NFTCollection): NFTDetailsUM = NFTDetailsUM(
|
||||
nftAsset = nftAsset.transform(nftCollection),
|
||||
pullToRefreshConfig = PullToRefreshConfig(
|
||||
isRefreshing = false,
|
||||
onRefresh = { onRefresh() },
|
||||
|
|
@ -42,63 +43,62 @@ internal class NFTDetailsUMFactory(
|
|||
onSendClick = onSendClick,
|
||||
)
|
||||
|
||||
private fun NFTAsset.transform(): NFTAssetUM = NFTAssetUM(
|
||||
name = name.orEmpty(),
|
||||
media = media?.imageUrl?.let {
|
||||
NFTAssetUM.Media.Content(
|
||||
url = it,
|
||||
)
|
||||
} ?: NFTAssetUM.Media.Empty,
|
||||
topInfo = when {
|
||||
!hasSalePrice() && description.isNullOrEmpty() && rarity == null -> {
|
||||
NFTAssetUM.TopInfo.Empty
|
||||
}
|
||||
else -> {
|
||||
val hasSalePrice = hasSalePrice()
|
||||
val hasDescription = !description.isNullOrEmpty()
|
||||
val rarity = rarity
|
||||
NFTAssetUM.TopInfo.Content(
|
||||
title = if (hasSalePrice) {
|
||||
resourceReference(R.string.nft_details_last_sale_price)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
salePrice = toSalePrice(),
|
||||
description = description,
|
||||
rarity = if (rarity != null) {
|
||||
NFTAssetUM.Rarity.Content(
|
||||
rank = rarity.rank,
|
||||
label = rarity.label,
|
||||
showDivider = hasSalePrice || hasDescription,
|
||||
onLabelClick = {
|
||||
onInfoBlockClick(
|
||||
resourceReference(R.string.nft_details_rarity_label),
|
||||
resourceReference(R.string.nft_details_info_rarity_label),
|
||||
)
|
||||
},
|
||||
onRankClick = {
|
||||
onInfoBlockClick(
|
||||
resourceReference(R.string.nft_details_rarity_rank),
|
||||
resourceReference(R.string.nft_details_info_rarity_rank),
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
NFTAssetUM.Rarity.Empty
|
||||
},
|
||||
private fun NFTAsset.transform(nftCollection: NFTCollection): NFTAssetUM {
|
||||
val description = description?.takeIf { it.isNotEmpty() } ?: nftCollection.description
|
||||
return NFTAssetUM(
|
||||
name = name.orEmpty(),
|
||||
media = media?.imageUrl?.let {
|
||||
NFTAssetUM.Media.Content(
|
||||
url = it,
|
||||
)
|
||||
}
|
||||
},
|
||||
traits = traits.take(MAX_TRAITS_COUNT).map {
|
||||
NFTAssetUM.BlockItem(
|
||||
title = stringReference(it.name),
|
||||
value = it.value,
|
||||
showInfoButton = false,
|
||||
)
|
||||
}.toImmutableList(),
|
||||
showAllTraitsButton = traits.size > MAX_TRAITS_COUNT,
|
||||
baseInfoItems = buildBaseInfoItems(),
|
||||
)
|
||||
} ?: NFTAssetUM.Media.Empty,
|
||||
topInfo = when {
|
||||
!hasSalePrice() && description.isNullOrEmpty() && rarity == null -> {
|
||||
NFTAssetUM.TopInfo.Empty
|
||||
}
|
||||
else -> {
|
||||
val hasSalePrice = hasSalePrice()
|
||||
val hasDescription = !description.isNullOrEmpty()
|
||||
val rarity = rarity
|
||||
NFTAssetUM.TopInfo.Content(
|
||||
title = resourceReference(R.string.nft_details_last_sale_price).takeIf { hasSalePrice },
|
||||
salePrice = toSalePrice(),
|
||||
description = description,
|
||||
rarity = if (rarity != null) {
|
||||
NFTAssetUM.Rarity.Content(
|
||||
rank = rarity.rank,
|
||||
label = rarity.label,
|
||||
showDivider = hasSalePrice || hasDescription,
|
||||
onLabelClick = {
|
||||
onInfoBlockClick(
|
||||
resourceReference(R.string.nft_details_rarity_label),
|
||||
resourceReference(R.string.nft_details_info_rarity_label),
|
||||
)
|
||||
},
|
||||
onRankClick = {
|
||||
onInfoBlockClick(
|
||||
resourceReference(R.string.nft_details_rarity_rank),
|
||||
resourceReference(R.string.nft_details_info_rarity_rank),
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
NFTAssetUM.Rarity.Empty
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
traits = traits.take(MAX_TRAITS_COUNT).map {
|
||||
NFTAssetUM.BlockItem(
|
||||
title = stringReference(it.name),
|
||||
value = it.value,
|
||||
showInfoButton = false,
|
||||
)
|
||||
}.toImmutableList(),
|
||||
showAllTraitsButton = traits.size > MAX_TRAITS_COUNT,
|
||||
baseInfoItems = buildBaseInfoItems(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun NFTAsset.hasSalePrice() = salePrice !is NFTSalePrice.Empty && salePrice !is NFTSalePrice.Error
|
||||
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ internal class NFTDetailsModel @Inject constructor(
|
|||
|
||||
private val _state by lazy {
|
||||
MutableStateFlow(
|
||||
value = stateFactory.getInitialState(params.nftAsset),
|
||||
value = stateFactory.getInitialState(params.nftAsset, params.nftCollection),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -187,7 +187,7 @@ internal class NFTDetailsModel @Inject constructor(
|
|||
AppRoute.NFTSend(
|
||||
userWalletId = params.userWalletId,
|
||||
nftAsset = params.nftAsset,
|
||||
nftCollectionName = params.nftCollectionName,
|
||||
nftCollectionName = params.nftCollection.name.orEmpty(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
package com.tangem.features.onramp.deeplink
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface BuyDeepLinkHandler {
|
||||
|
||||
interface Factory {
|
||||
fun create(coroutineScope: CoroutineScope): BuyDeepLinkHandler
|
||||
fun create(): BuyDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.onramp.deeplink
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface BuyRedirectDeepLinkHandler {
|
||||
|
||||
interface Factory {
|
||||
fun create(coroutineScope: CoroutineScope): BuyRedirectDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.features.onramp.deeplink
|
||||
|
||||
interface SellDeepLinkHandler {
|
||||
|
||||
interface Factory {
|
||||
fun create(): SellDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.features.onramp.deeplink
|
||||
|
||||
interface SwapDeepLinkHandler {
|
||||
|
||||
interface Factory {
|
||||
fun create(): SwapDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +1,15 @@
|
|||
package com.tangem.features.onramp.deeplink
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
|
||||
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
|
||||
import com.tangem.domain.wallets.models.isMultiCurrency
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.features.onramp.OnrampFeatureToggles
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultBuyDeepLinkHandler @AssistedInject constructor(
|
||||
@Assisted scope: CoroutineScope,
|
||||
onrampFeatureToggles: OnrampFeatureToggles,
|
||||
router: AppRouter,
|
||||
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : BuyDeepLinkHandler {
|
||||
|
||||
init {
|
||||
|
|
@ -29,21 +19,13 @@ internal class DefaultBuyDeepLinkHandler @AssistedInject constructor(
|
|||
Timber.e("Error on getting user wallet: $it")
|
||||
},
|
||||
ifRight = { userWallet ->
|
||||
if (!onrampFeatureToggles.isFeatureEnabled && !userWallet.isMultiCurrency) {
|
||||
scope.launch {
|
||||
val cryptoCurrency = getCryptoCurrencyUseCase(userWallet.walletId).getOrElse {
|
||||
Timber.e("Error on getting cryptoCurrency: $it")
|
||||
return@launch
|
||||
}
|
||||
analyticsEventHandler.send(TokenScreenAnalyticsEvent.Bought(cryptoCurrency.symbol))
|
||||
}
|
||||
}
|
||||
router.push(AppRoute.BuyCrypto(userWallet.walletId))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : BuyDeepLinkHandler.Factory {
|
||||
override fun create(coroutineScope: CoroutineScope): DefaultBuyDeepLinkHandler
|
||||
override fun create(): DefaultBuyDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.features.onramp.deeplink
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
|
||||
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
|
||||
import com.tangem.domain.wallets.models.isMultiCurrency
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.features.onramp.OnrampFeatureToggles
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultBuyRedirectDeepLinkHandler @AssistedInject constructor(
|
||||
@Assisted scope: CoroutineScope,
|
||||
onrampFeatureToggles: OnrampFeatureToggles,
|
||||
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : BuyRedirectDeepLinkHandler {
|
||||
|
||||
init {
|
||||
// It is okay here, we are navigating from outside, and there is no other way to getting UserWallet
|
||||
getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = {
|
||||
Timber.e("Error on getting user wallet: $it")
|
||||
},
|
||||
ifRight = { userWallet ->
|
||||
if (!onrampFeatureToggles.isFeatureEnabled && !userWallet.isMultiCurrency) {
|
||||
scope.launch {
|
||||
val cryptoCurrency = getCryptoCurrencyUseCase(userWallet.walletId).getOrElse {
|
||||
Timber.e("Error on getting cryptoCurrency: $it")
|
||||
return@launch
|
||||
}
|
||||
analyticsEventHandler.send(TokenScreenAnalyticsEvent.Bought(cryptoCurrency.symbol))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : BuyRedirectDeepLinkHandler.Factory {
|
||||
override fun create(coroutineScope: CoroutineScope): DefaultBuyRedirectDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.features.onramp.deeplink
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultSellDeepLinkHandler @AssistedInject constructor(
|
||||
router: AppRouter,
|
||||
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
) : SellDeepLinkHandler {
|
||||
|
||||
init {
|
||||
// It is okay here, we are navigating from outside, and there is no other way to getting UserWallet
|
||||
getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = {
|
||||
Timber.e("Error on getting user wallet: $it")
|
||||
},
|
||||
ifRight = { userWallet ->
|
||||
router.push(AppRoute.SellCrypto(userWallet.walletId))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SellDeepLinkHandler.Factory {
|
||||
override fun create(): DefaultSellDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.features.onramp.deeplink
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultSwapDeepLinkHandler @AssistedInject constructor(
|
||||
router: AppRouter,
|
||||
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
) : SwapDeepLinkHandler {
|
||||
|
||||
init {
|
||||
// It is okay here, we are navigating from outside, and there is no other way to getting UserWallet
|
||||
getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = {
|
||||
Timber.e("Error on getting user wallet: $it")
|
||||
},
|
||||
ifRight = { userWallet ->
|
||||
router.push(AppRoute.SwapCrypto(userWallet.walletId))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SwapDeepLinkHandler.Factory {
|
||||
override fun create(): DefaultSwapDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,21 @@ internal interface OnrampDeeplinkModule {
|
|||
@Singleton
|
||||
fun bindOnrampDeepLinkHandlerFactory(impl: DefaultOnrampDeepLinkHandler.Factory): OnrampDeepLinkHandler.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindBuyRedirectDeepLinkHandler(
|
||||
impl: DefaultBuyRedirectDeepLinkHandler.Factory,
|
||||
): BuyRedirectDeepLinkHandler.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindBuyDeepLinkHandler(impl: DefaultBuyDeepLinkHandler.Factory): BuyDeepLinkHandler.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindSellDeepLinkHandler(impl: DefaultSellDeepLinkHandler.Factory): SellDeepLinkHandler.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindSwapDeepLinkHandler(impl: DefaultSwapDeepLinkHandler.Factory): SwapDeepLinkHandler.Factory
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ dependencies {
|
|||
implementation(projects.core.utils)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.deepLinks)
|
||||
implementation(projects.libs.crypto)
|
||||
implementation(projects.common.routing)
|
||||
|
||||
|
|
@ -41,6 +42,7 @@ dependencies {
|
|||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.notifications.models)
|
||||
implementation(projects.features.referral.domain)
|
||||
|
||||
/** Other libraries */
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ package com.tangem.features.send.v2.api.deeplink
|
|||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface SellDeepLinkHandler {
|
||||
interface SellRedirectDeepLinkHandler {
|
||||
|
||||
interface Factory {
|
||||
fun create(coroutineScope: CoroutineScope, queryParams: Map<String, String>): SellDeepLinkHandler
|
||||
fun create(coroutineScope: CoroutineScope, queryParams: Map<String, String>): SellRedirectDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import com.tangem.common.routing.AppRoute
|
|||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler
|
||||
import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -14,13 +14,13 @@ import kotlinx.coroutines.launch
|
|||
import timber.log.Timber
|
||||
|
||||
@Suppress("ComplexCondition")
|
||||
internal class DefaultSellDeepLinkHandler @AssistedInject constructor(
|
||||
internal class DefaultSellRedirectDeepLinkHandler @AssistedInject constructor(
|
||||
@Assisted scope: CoroutineScope,
|
||||
@Assisted queryParams: Map<String, String>,
|
||||
appRouter: AppRouter,
|
||||
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
|
||||
) : SellDeepLinkHandler {
|
||||
) : SellRedirectDeepLinkHandler {
|
||||
|
||||
init {
|
||||
val currencyId = queryParams[CURRENCY_ID_KEY]
|
||||
|
|
@ -70,11 +70,11 @@ internal class DefaultSellDeepLinkHandler @AssistedInject constructor(
|
|||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SellDeepLinkHandler.Factory {
|
||||
interface Factory : SellRedirectDeepLinkHandler.Factory {
|
||||
override fun create(
|
||||
coroutineScope: CoroutineScope,
|
||||
queryParams: Map<String, String>,
|
||||
): DefaultSellDeepLinkHandler
|
||||
): DefaultSellRedirectDeepLinkHandler
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.send.v2.deeplink.di
|
||||
|
||||
import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler
|
||||
import com.tangem.features.send.v2.deeplink.DefaultSellDeepLinkHandler
|
||||
import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler
|
||||
import com.tangem.features.send.v2.deeplink.DefaultSellRedirectDeepLinkHandler
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -14,5 +14,5 @@ internal interface SendDeepLinkModule {
|
|||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindFactory(impl: DefaultSellDeepLinkHandler.Factory): SellDeepLinkHandler.Factory
|
||||
fun bindFactory(impl: DefaultSellRedirectDeepLinkHandler.Factory): SellRedirectDeepLinkHandler.Factory
|
||||
}
|
||||
|
|
@ -47,6 +47,7 @@ dependencies {
|
|||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.deepLinks)
|
||||
|
||||
|
||||
/** Domain */
|
||||
|
|
@ -66,6 +67,7 @@ dependencies {
|
|||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.feedback)
|
||||
implementation(projects.domain.feedback.models)
|
||||
implementation(projects.domain.notifications.models)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,12 @@ package com.tangem.features.staking.impl.deeplink
|
|||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.deeplink.DeeplinkConst.NETWORK_ID_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
|
||||
import com.tangem.core.deeplink.DeeplinkConst.WALLET_ID_KEY
|
||||
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
|
||||
import com.tangem.domain.staking.GetYieldUseCase
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
|
|
@ -15,6 +20,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
|
||||
@Assisted private val scope: CoroutineScope,
|
||||
@Assisted private val queryParams: Map<String, String>,
|
||||
|
|
@ -22,6 +28,7 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
|
|||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
|
||||
private val getYieldUseCase: GetYieldUseCase,
|
||||
private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase,
|
||||
) : StakingDeepLinkHandler {
|
||||
|
||||
init {
|
||||
|
|
@ -29,24 +36,28 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun handleDeepLink() {
|
||||
// It is okay here, we are navigating from outside, and there is no other way to getting UserWallet
|
||||
val userWalletId = queryParams[WALLET_ID_KEY]?.let(::UserWalletId)
|
||||
?: getSelectedWalletSyncUseCase().getOrNull()?.walletId
|
||||
val networkId = queryParams[NETWORK_ID_KEY]
|
||||
val tokenId = queryParams[TOKEN_ID_KEY]
|
||||
|
||||
if (userWalletId == null) {
|
||||
// It is okay here, we are navigating from outside, and there is no other way to getting UserWallet
|
||||
val selectedUserWalletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId
|
||||
val walletId = queryParams[WALLET_ID_KEY]?.let(::UserWalletId) ?: selectedUserWalletId
|
||||
|
||||
// If selected user wallet is different than from deeplink - ignore deeplink
|
||||
// If selected user wallet is null - ignore deeplink
|
||||
if (walletId != selectedUserWalletId || selectedUserWalletId == null) {
|
||||
Timber.e("Error on getting user wallet")
|
||||
return
|
||||
}
|
||||
|
||||
val networkId = queryParams[NETWORK_ID_KEY]
|
||||
val tokenId = queryParams[TOKEN_ID_KEY]
|
||||
|
||||
scope.launch {
|
||||
val cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId = userWalletId).getOrElse {
|
||||
val cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId = selectedUserWalletId).getOrElse {
|
||||
Timber.e("Error on getting crypto currency list")
|
||||
return@launch
|
||||
}.firstOrNull {
|
||||
it.network.backendId == networkId && it.id.rawCurrencyId?.value == tokenId
|
||||
val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true)
|
||||
val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true
|
||||
isNetwork && isCurrency
|
||||
}
|
||||
|
||||
if (cryptoCurrency == null) {
|
||||
|
|
@ -60,6 +71,15 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
|
|||
return@launch
|
||||
}
|
||||
|
||||
val isStakingEnabled = getStakingAvailabilityUseCase.invokeSync(
|
||||
userWalletId = selectedUserWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
).getOrNull()
|
||||
|
||||
if (isStakingEnabled !is StakingAvailability.Available) {
|
||||
return@launch
|
||||
}
|
||||
|
||||
val yield = getYieldUseCase.invoke(
|
||||
cryptoCurrencyId = cryptoCurrency.id,
|
||||
symbol = cryptoCurrency.symbol,
|
||||
|
|
@ -70,7 +90,7 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
|
|||
|
||||
appRouter.push(
|
||||
AppRoute.Staking(
|
||||
userWalletId = userWalletId,
|
||||
userWalletId = selectedUserWalletId,
|
||||
cryptoCurrencyId = cryptoCurrency.id,
|
||||
yieldId = yield.id,
|
||||
),
|
||||
|
|
@ -85,10 +105,4 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
|
|||
queryParams: Map<String, String>,
|
||||
): DefaultStakingDeepLinkHandler
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val WALLET_ID_KEY = "walletId"
|
||||
const val NETWORK_ID_KEY = "network_id"
|
||||
const val TOKEN_ID_KEY = "token_id"
|
||||
}
|
||||
}
|
||||
|
|
@ -19,8 +19,7 @@ internal class TestPushMarketTokenClickBottomSheetTransformer(
|
|||
append(DeepLinkRoute.MarketTokenDetail.host)
|
||||
append("?token_id=")
|
||||
append(tokenMarket.id)
|
||||
append("&token_symbol=")
|
||||
append(tokenMarket.symbol)
|
||||
append("&type=promo")
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -39,56 +39,57 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun handleDeepLink() {
|
||||
// It is okay here, we are navigating from outside, and there is no other way to getting UserWallet
|
||||
val userWalletId = queryParams[WALLET_ID_KEY]?.let(::UserWalletId)
|
||||
?: getSelectedWalletSyncUseCase().getOrNull()?.walletId
|
||||
|
||||
if (userWalletId == null) {
|
||||
Timber.e("Error on getting user wallet")
|
||||
return
|
||||
}
|
||||
|
||||
val networkId = queryParams[NETWORK_ID_KEY]
|
||||
val tokenId = queryParams[TOKEN_ID_KEY]
|
||||
val type = NotificationType.getType(queryParams[TYPE_KEY])
|
||||
|
||||
if (type != NotificationType.Unknown) {
|
||||
scope.launch {
|
||||
val cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId = userWalletId).getOrElse {
|
||||
Timber.e("Error on getting crypto currency list")
|
||||
return@launch
|
||||
}.firstOrNull {
|
||||
it.network.backendId == networkId &&
|
||||
it.id.rawCurrencyId?.value == tokenId
|
||||
}
|
||||
// It is okay here, we are navigating from outside, and there is no other way to getting UserWallet
|
||||
val selectedUserWalletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId
|
||||
val walletId = queryParams[WALLET_ID_KEY]?.let(::UserWalletId) ?: selectedUserWalletId
|
||||
|
||||
if (cryptoCurrency == null) {
|
||||
Timber.e(
|
||||
"""
|
||||
// If selected user wallet is different than from deeplink - ignore deeplink
|
||||
// If selected user wallet is null - ignore deeplink
|
||||
if (walletId != selectedUserWalletId || selectedUserWalletId == null) {
|
||||
Timber.e("Error on getting user wallet")
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
val cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId = selectedUserWalletId).getOrElse {
|
||||
Timber.e("Error on getting crypto currency list")
|
||||
return@launch
|
||||
}.firstOrNull {
|
||||
val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true)
|
||||
val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true
|
||||
isNetwork && isCurrency
|
||||
}
|
||||
|
||||
if (cryptoCurrency == null) {
|
||||
Timber.e(
|
||||
"""
|
||||
Could not get crypto currency for
|
||||
|- $NETWORK_ID_KEY: $networkId
|
||||
|- $TOKEN_ID_KEY: $tokenId
|
||||
""".trimIndent(),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
analyticsEventHandler.send(PushNotificationAnalyticEvents.NotificationOpened(type.name))
|
||||
|
||||
appRouter.push(
|
||||
AppRoute.CurrencyDetails(
|
||||
userWalletId = userWalletId,
|
||||
currency = cryptoCurrency,
|
||||
),
|
||||
""".trimIndent(),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (isFromOnNewIntent) {
|
||||
fetchCurrencyStatusUseCase.invoke(
|
||||
userWalletId = userWalletId,
|
||||
id = cryptoCurrency.id,
|
||||
refresh = true,
|
||||
)
|
||||
}
|
||||
analyticsEventHandler.send(PushNotificationAnalyticEvents.NotificationOpened(type.name))
|
||||
|
||||
appRouter.push(
|
||||
AppRoute.CurrencyDetails(
|
||||
userWalletId = selectedUserWalletId,
|
||||
currency = cryptoCurrency,
|
||||
),
|
||||
)
|
||||
|
||||
if (isFromOnNewIntent) {
|
||||
fetchCurrencyStatusUseCase.invoke(
|
||||
userWalletId = selectedUserWalletId,
|
||||
id = cryptoCurrency.id,
|
||||
refresh = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
package com.tangem.feature.wallet.deeplink
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultWalletDeepLinkHandler @AssistedInject constructor() : WalletDeepLinkHandler {
|
||||
internal class DefaultWalletDeepLinkHandler @AssistedInject constructor(
|
||||
router: AppRouter,
|
||||
) : WalletDeepLinkHandler {
|
||||
|
||||
init {
|
||||
router.popTo(AppRoute.Wallet)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : WalletDeepLinkHandler.Factory {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue