Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-18 18:55:47 +05:00
parent b7dc46ae64
commit 9df1b84404
13 changed files with 261 additions and 11 deletions

View file

@ -24,8 +24,10 @@ import com.tangem.features.onramp.component.*
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.api.SendEntryPointComponent
import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.swap.SwapComponent
import com.tangem.features.swap.v2.api.SendWithSwapComponent
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.wallet.WalletEntryComponent
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
@ -85,6 +87,8 @@ internal class ChildFactory @Inject constructor(
private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory,
private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory,
private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory,
private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory,
private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
) {
@ -442,6 +446,26 @@ internal class ChildFactory @Inject constructor(
componentFactory = addExistingWalletComponentFactory,
)
}
is AppRoute.SendEntryPoint -> {
createComponentChild(
context = context,
params = SendEntryPointComponent.Params(
userWalletId = route.userWalletId,
cryptoCurrency = route.currency,
),
componentFactory = sendEntryPointComponentFactory,
)
}
is AppRoute.SendWithSwap -> {
createComponentChild(
context = context,
params = SendWithSwapComponent.Params(
userWalletId = route.userWalletId,
currency = route.currency,
),
componentFactory = sendWithSwapComponentFactory,
)
}
}
}
}

View file

@ -288,4 +288,18 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
object AddExistingWallet : AppRoute(path = "/add_existing_wallet")
@Serializable
data class SendEntryPoint(
val userWalletId: UserWalletId,
val currency: CryptoCurrency,
) : AppRoute(
path = "/send_entry_point/${userWalletId.stringValue}/${currency.id.value}?",
)
@Serializable
data class SendWithSwap(
val userWalletId: UserWalletId,
val currency: CryptoCurrency,
) : AppRoute(path = "/send_with_swap/${userWalletId.stringValue}/${currency.symbol}")
}

View file

@ -15,6 +15,7 @@ dependencies {
/* Project - API */
api(projects.features.markets.api)
api(projects.features.onramp.api)
api(projects.features.sendV2.api)
/* Data */
implementation(projects.data.common)

View file

@ -23,6 +23,7 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.utils.Provider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -41,6 +42,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
private val isDemoCardUseCase: IsDemoCardUseCase,
private val messageSender: UiMessageSender,
private val shareManager: ShareManager,
private val sendFeatureToggles: SendFeatureToggles,
) {
private val disabledActionsInDemoMode = buildSet {
@ -157,12 +159,18 @@ internal class TokenActionsHandler @AssistedInject constructor(
}
private fun onSendClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
router.push(
val route = if (sendFeatureToggles.isSendWithSwapEnabled) {
AppRoute.SendEntryPoint(
userWalletId = cryptoCurrencyData.userWallet.walletId,
currency = cryptoCurrencyData.status.currency,
)
} else {
AppRoute.Send(
userWalletId = cryptoCurrencyData.userWallet.walletId,
currency = cryptoCurrencyData.status.currency,
),
)
)
}
router.push(route)
}
private fun onStakeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {

View file

@ -0,0 +1,16 @@
package com.tangem.features.send.v2.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
interface SendEntryPointComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency,
)
interface Factory : ComponentFactory<Params, SendEntryPointComponent>
}

View file

@ -20,6 +20,7 @@ dependencies {
implementation(projects.features.sendV2.api)
implementation(projects.features.txhistory.api)
implementation(projects.features.nft.api)
implementation(projects.features.swapV2.api)
/** Libs */
implementation(projects.libs.crypto)
@ -64,6 +65,7 @@ dependencies {
implementation(projects.domain.nft.models)
implementation(projects.domain.nft)
implementation(projects.domain.notifications)
implementation(projects.domain.swap.models)
/** Compose libraries */

View file

@ -2,10 +2,8 @@ package com.tangem.features.send.v2.di
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.send.v2.DefaultSendFeatureToggles
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.api.SendNotificationsComponent
import com.tangem.features.send.v2.api.*
import com.tangem.features.send.v2.entrypoint.DefaultSendEntryPointComponent
import com.tangem.features.send.v2.send.DefaultSendComponent
import com.tangem.features.send.v2.sendnft.DefaultNFTSendComponent
import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent
@ -43,4 +41,10 @@ internal interface SendFeatureModuleBinds {
fun provideNotificationComponentFactory(
impl: DefaultSendNotificationsComponent.Factory,
): SendNotificationsComponent.Factory
@Binds
@Singleton
fun provideSendEntryPointComponentFactory(
impl: DefaultSendEntryPointComponent.Factory,
): SendEntryPointComponent.Factory
}

View file

@ -0,0 +1,71 @@
package com.tangem.features.send.v2.entrypoint
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.api.SendEntryPointComponent
import com.tangem.features.send.v2.entrypoint.model.SendEntryPoint
import com.tangem.features.send.v2.entrypoint.model.SendEntryPointModel
import com.tangem.features.swap.v2.api.SendWithSwapComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultSendEntryPointComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: SendEntryPointComponent.Params,
sendWithSwapComponentFactory: SendWithSwapComponent.Factory,
sendComponentFactory: SendComponent.Factory,
) : SendEntryPointComponent, AppComponentContext by appComponentContext {
private val model: SendEntryPointModel = getOrCreateModel(params = params)
private val sendWithSwapComponent = sendWithSwapComponentFactory.create(
context = child("sendEntrySendWithSwap"),
params = SendWithSwapComponent.Params(
userWalletId = params.userWalletId,
currency = params.cryptoCurrency,
callback = model,
),
)
private val sendComponent = sendComponentFactory.create(
context = child("sendEntryVanillaSend"),
params = SendComponent.Params(
userWalletId = params.userWalletId,
currency = params.cryptoCurrency,
callback = model,
),
)
@Composable
override fun Content(modifier: Modifier) {
val sendEntryState by model.sendEntryPointState.collectAsStateWithLifecycle()
sendComponent.Content(modifier)
AnimatedVisibility(
visible = sendEntryState == SendEntryPoint.SendWithSwap,
enter = fadeIn(),
exit = fadeOut(),
) {
sendWithSwapComponent.Content(modifier)
}
}
@AssistedFactory
interface Factory : SendEntryPointComponent.Factory {
override fun create(
context: AppComponentContext,
params: SendEntryPointComponent.Params,
): DefaultSendEntryPointComponent
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.send.v2.entrypoint.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.send.v2.entrypoint.model.SendEntryPointModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface SendEntryPointModule {
@Binds
@IntoMap
@ClassKey(SendEntryPointModel::class)
fun provideSendEntryPointModel(model: SendEntryPointModel): Model
}

View file

@ -0,0 +1,79 @@
package com.tangem.features.send.v2.entrypoint.model
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.api.SendEntryPointComponent
import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateTrigger
import com.tangem.features.swap.v2.api.SendWithSwapComponent
import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkListener
import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import jakarta.inject.Inject
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ModelScoped
internal class SendEntryPointModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
val appRouter: AppRouter,
private val swapChooseTokenNetworkListener: SwapChooseTokenNetworkListener,
private val sendAmountUpdateTrigger: SendAmountUpdateTrigger,
private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger,
) : Model(), SendComponent.ModelCallback, SendWithSwapComponent.ModelCallback {
private val params: SendEntryPointComponent.Params = paramsContainer.require()
val sendEntryPointState: StateFlow<SendEntryPoint>
field = MutableStateFlow(SendEntryPoint.SendVanilla)
private var swapChooseTokenListenerJobHolder = JobHolder()
override fun onConvertToAnotherToken(lastAmount: String) {
appRouter.push(
AppRoute.ChooseManagedTokens(
userWalletId = params.userWalletId,
initialCurrency = params.cryptoCurrency,
source = AppRoute.ChooseManagedTokens.Source.SendViaSwap,
),
)
observeChooseSelectToken(lastAmount)
}
override fun onCloseSwap(lastAmount: String) {
modelScope.launch {
if (lastAmount.isNotBlank()) {
sendAmountUpdateTrigger.triggerUpdateAmount(lastAmount)
}
triggerScreenUpdate(SendEntryPoint.SendVanilla)
}
}
private fun observeChooseSelectToken(lastAmount: String) {
swapChooseTokenNetworkListener.swapChooseTokenNetworkResultFlow
.onEach { currency ->
if (lastAmount.isNotBlank()) {
swapAmountUpdateTrigger.triggerUpdateAmount(lastAmount)
}
triggerScreenUpdate(SendEntryPoint.SendWithSwap)
}
.launchIn(modelScope)
.saveIn(swapChooseTokenListenerJobHolder)
}
private fun triggerScreenUpdate(entry: SendEntryPoint) {
swapChooseTokenListenerJobHolder.cancel()
sendEntryPointState.update { entry }
}
}
enum class SendEntryPoint {
SendVanilla,
SendWithSwap,
}

View file

@ -101,5 +101,6 @@ dependencies {
implementation(projects.features.pushNotifications.api)
implementation(projects.features.swap.api)
implementation(projects.features.txhistory.api)
implementation(projects.features.sendV2.api)
}

View file

@ -80,6 +80,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBala
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.ExpressStatusFactory
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.tokendetails.impl.R
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
@ -137,6 +138,7 @@ internal class TokenDetailsModel @Inject constructor(
private val router: InnerTokenDetailsRouter,
private val tokenDetailsDeepLinkActionListener: TokenDetailsDeepLinkActionListener,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
private val sendFeatureToggles: SendFeatureToggles,
) : Model(), TokenDetailsClickIntents {
private val params = paramsContainer.require<TokenDetailsComponent.Params>()
@ -564,10 +566,17 @@ internal class TokenDetailsModel @Inject constructor(
}
private fun sendCurrency() {
val route = AppRoute.Send(
currency = cryptoCurrency,
userWalletId = userWallet.walletId,
)
val route = if (sendFeatureToggles.isSendWithSwapEnabled) {
AppRoute.SendEntryPoint(
currency = cryptoCurrency,
userWalletId = userWallet.walletId,
)
} else {
AppRoute.Send(
currency = cryptoCurrency,
userWalletId = userWallet.walletId,
)
}
appRouter.push(route)
}

View file

@ -110,6 +110,7 @@ dependencies {
implementation(projects.features.walletSettings.api)
implementation(projects.features.biometry.api)
implementation(projects.features.nft.api)
implementation(projects.features.sendV2.api)
/** Common modules */
implementation(projects.common)