Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-04 19:31:21 +05:00
parent 469c0a4317
commit 6c8285847f
11 changed files with 347 additions and 12 deletions

View file

@ -19,6 +19,7 @@ import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onramp.component.*
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.send.api.SendComponent
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.swap.SwapComponent
@ -86,6 +87,7 @@ internal class ChildFactory @Inject constructor(
private val nftCollectionsComponentFactory: NFTCollectionsComponent.Factory,
private val nftReceiveComponentFactory: NFTReceiveComponent.Factory,
private val nftDetailsComponentFactory: NFTDetailsComponent.Factory,
private val nftSendComponentFactory: NFTSendComponent.Factory,
private val testerRouter: TesterRouter,
private val routingFeatureToggles: RoutingFeatureToggles,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
@ -423,6 +425,17 @@ internal class ChildFactory @Inject constructor(
params = NFTDetailsComponent.Params(userWalletId = route.userWalletId, nftAsset = route.nftAsset),
componentFactory = nftDetailsComponentFactory,
)
is AppRoute.NFTSend -> {
createComponentChild(
context = context,
params = NFTSendComponent.Params(
userWalletId = route.userWalletId,
nftAsset = route.nftAsset,
nftCollectionName = route.nftCollectionName,
),
componentFactory = nftSendComponentFactory,
)
}
is AppRoute.OnboardingNote,
is AppRoute.SaveWallet,
is AppRoute.OnboardingOther,
@ -775,6 +788,17 @@ internal class ChildFactory @Inject constructor(
params = NFTDetailsComponent.Params(userWalletId = route.userWalletId, nftAsset = route.nftAsset),
componentFactory = nftDetailsComponentFactory,
)
is AppRoute.NFTSend -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = NFTSendComponent.Params(
userWalletId = route.userWalletId,
nftAsset = route.nftAsset,
nftCollectionName = route.nftCollectionName,
),
componentFactory = nftSendComponentFactory,
)
}
}
// endregion
}

View file

@ -295,4 +295,11 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
val nftAsset: NFTAsset,
) : AppRoute(path = "/nft_details/${userWalletId.stringValue}/${nftAsset.collectionId}/${nftAsset.id.stringValue}")
@Serializable
data class NFTSend(
val userWalletId: UserWalletId,
val nftAsset: NFTAsset,
val nftCollectionName: String,
) : AppRoute(path = "/send/nft/${userWalletId.stringValue}/$nftCollectionName/${nftAsset.id}")
}

View file

@ -16,4 +16,5 @@ dependencies {
/** Domain models */
implementation(projects.domain.wallets.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.nft.models)
}

View file

@ -0,0 +1,17 @@
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.nft.models.NFTAsset
import com.tangem.domain.wallets.models.UserWalletId
interface NFTSendComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val nftAsset: NFTAsset,
val nftCollectionName: String,
)
interface Factory : ComponentFactory<Params, NFTSendComponent>
}

View file

@ -2,9 +2,11 @@ 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.send.DefaultSendComponent
import com.tangem.features.send.v2.sendnft.DefaultNFTSendComponent
import dagger.Binds
import dagger.Module
import dagger.Provides
@ -29,4 +31,8 @@ internal interface SendFeatureModuleBinds {
@Binds
@Singleton
fun provideSendComponentFactory(impl: DefaultSendComponent.Factory): SendComponent.Factory
@Binds
@Singleton
fun provideNFTSendComponentFactory(impl: DefaultNFTSendComponent.Factory): NFTSendComponent.Factory
}

View file

@ -2,8 +2,6 @@ package com.tangem.features.send.v2.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.send.v2.send.confirm.model.SendConfirmModel
import com.tangem.features.send.v2.send.model.SendModel
import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel
import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeModel
@ -18,11 +16,6 @@ import dagger.multibindings.IntoMap
@InstallIn(ModelComponent::class)
internal interface SendModelModule {
@Binds
@IntoMap
@ClassKey(SendModel::class)
fun provideSendModel(model: SendModel): Model
@Binds
@IntoMap
@ClassKey(SendAmountModel::class)
@ -38,11 +31,6 @@ internal interface SendModelModule {
@ClassKey(SendFeeModel::class)
fun provideSendFeeModel(model: SendFeeModel): Model
@Binds
@IntoMap
@ClassKey(SendConfirmModel::class)
fun provideSendConfirmModel(model: SendConfirmModel): Model
@Binds
@IntoMap
@ClassKey(NotificationsModel::class)

View file

@ -0,0 +1,26 @@
package com.tangem.features.send.v2.send.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.send.v2.send.confirm.model.SendConfirmModel
import com.tangem.features.send.v2.send.model.SendModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface SendModelModule {
@Binds
@IntoMap
@ClassKey(SendModel::class)
fun provideSendModel(model: SendModel): Model
@Binds
@IntoMap
@ClassKey(SendConfirmModel::class)
fun provideSendConfirmModel(model: SendConfirmModel): Model
}

View file

@ -0,0 +1,89 @@
package com.tangem.features.send.v2.sendnft
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.sendnft.model.NFTSendModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultNFTSendComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: NFTSendComponent.Params,
) : NFTSendComponent, AppComponentContext by appComponentContext {
// private val stackNavigation = StackNavigation<NFTSendRoute>()
//
// private val innerRouter = InnerRouter<NFTSendRoute>(
// stackNavigation = stackNavigation,
// popCallback = { onChildBack() },
// )
// private val initialRoute = NFTSendRoute.Empty
// private val currentRouteFlow = MutableStateFlow<NFTSendRoute>(initialRoute)
private val model: NFTSendModel = getOrCreateModel(params = params/*, router = innerRouter*/)
// private val childStack = childStack(
// key = "NFTSendInnerStack",
// source = stackNavigation,
// serializer = null,
// initialConfiguration = initialRoute,
// handleBackButton = true,
// childFactory = { configuration, factoryContext ->
// // todo
// ComposableContentComponent { }
// },
// )
init {
// childStack.subscribe(
// lifecycle = lifecycle,
// mode = ObserveLifecycleMode.CREATE_DESTROY,
// ) { stack ->
// componentScope.launch {
// when (val activeComponent = stack.active.instance) {
// is SendDestinationComponent -> {
// // analyticsEventHandler.send(SendAnalyticEvents.AddressScreenOpened)
// activeComponent.updateState(model.uiState.value.destinationUM)
// }
// is SendFeeComponent -> {
// // analyticsEventHandler.send(SendAnalyticEvents.FeeScreenOpened)
// }
// }
// currentRouteFlow.emit(stack.active.configuration)
// }
// }
}
@Composable
override fun Content(modifier: Modifier) {
// val stackState by childStack.subscribeAsState()
// val state by model.uiState.collectAsStateWithLifecycle()
BackHandler(onBack = ::onChildBack)
// TODO
}
private fun onChildBack() {
// TODO
// val isEmptyRoute = childStack.value.active.configuration == NFTSendRoute.Empty
// val isEmptyStack = childStack.value.backStack.isEmpty()
// val isSuccess = model.uiState.value.confirmUM is ConfirmUM.Success
//
// if (isEmptyRoute || isEmptyStack || isSuccess) {
// router.pop()
// } else {
// stackNavigation.pop()
// }
}
@AssistedFactory
interface Factory : NFTSendComponent.Factory {
override fun create(context: AppComponentContext, params: NFTSendComponent.Params): DefaultNFTSendComponent
}
}

View file

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

View file

@ -0,0 +1,144 @@
package com.tangem.features.send.v2.sendnft.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.properties.Delegates
internal interface SendNFTComponentCallback :
SendFeeComponent.ModelCallback,
SendDestinationComponent.ModelCallback
@Suppress("LongParameterList")
@Stable
@ModelScoped
internal class NFTSendModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
) : Model(), SendNFTComponentCallback {
val params: NFTSendComponent.Params = paramsContainer.require()
private val userWalletId = params.userWalletId
private val _uiState = MutableStateFlow(initialState())
val uiState = _uiState.asStateFlow()
private val _isBalanceHiddenFlow = MutableStateFlow(false)
val isBalanceHiddenFlow = _isBalanceHiddenFlow.asStateFlow()
var cryptoCurrency: CryptoCurrency by Delegates.notNull()
var userWallet: UserWallet by Delegates.notNull()
var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
var appCurrency: AppCurrency = AppCurrency.Default
init {
subscribeOnCurrencyStatusUpdates()
initAppCurrency()
}
override fun onNavigationResult(navigationUM: NavigationUM) {
_uiState.update { it.copy(navigationUM = navigationUM) }
}
override fun onDestinationResult(destinationUM: DestinationUM) {
_uiState.update { it.copy(destinationUM = destinationUM) }
// todo
}
override fun onFeeResult(feeUM: FeeUM) {
_uiState.update { it.copy(feeUM = feeUM) }
router.pop()
}
private fun initAppCurrency() {
modelScope.launch {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
}
}
private fun subscribeOnCurrencyStatusUpdates() {
modelScope.launch {
getUserWalletUseCase(params.userWalletId).fold(
ifRight = { wallet ->
userWallet = wallet
// cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId).getOrNull()
// ?.filterIsInstance<CryptoCurrency.Coin>()
// ?.firstOrNull { it.network == nftAsset.network }
// ?: return@launch
getCurrenciesStatusUpdates(
isSingleWalletWithToken = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(),
)
},
ifLeft = {
// sendConfirmAlertFactory.getGenericErrorState(::onFailedTxEmailClick)
return@launch
},
)
}
}
private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean) {
getCurrencyStatusUpdatesUseCase(
userWalletId = userWalletId,
currencyId = cryptoCurrency.id,
isSingleWalletWithTokens = isSingleWalletWithToken,
).onEach { maybeCryptoCurrency ->
maybeCryptoCurrency.fold(
ifRight = { cryptoStatus ->
cryptoCurrencyStatus = cryptoStatus
feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoStatus,
).getOrNull() ?: cryptoStatus
// router.push(NFTSendRoute.Destination(isEditMode = false))
},
ifLeft = {
// sendConfirmAlertFactory.getGenericErrorState {
// onFailedTxEmailClick(it.toString())
// }
},
)
}.launchIn(modelScope)
}
private fun initialState(): NFTSendUM = NFTSendUM(
destinationUM = DestinationUM.Empty(),
feeUM = FeeUM.Empty(),
confirmUM = ConfirmUM.Empty,
navigationUM = NavigationUM.Empty,
)
}

View file

@ -0,0 +1,13 @@
package com.tangem.features.send.v2.sendnft.ui.state
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
internal data class NFTSendUM(
val destinationUM: DestinationUM,
val feeUM: FeeUM,
val confirmUM: ConfirmUM,
val navigationUM: NavigationUM,
)