diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 237b83807e..99bb5efeef 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -427,4 +427,10 @@ internal object TokensDomainModule { tokensFeatureToggles = tokensFeatureToggles, ) } + + @Provides + @Singleton + fun provideGetCryptoCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase { + return GetCryptoCurrenciesUseCase(currenciesRepository) + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt new file mode 100644 index 0000000000..72b4d94cc4 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId + +class GetCryptoCurrenciesUseCase( + private val currenciesRepository: CurrenciesRepository, +) { + + /** + * Retrieves the list of cryptocurrencies within a multi-currency wallet. + * + * @param userWalletId The unique identifier of the user wallet. + * + * @return An [Either] representing success (Right) or an error (Left) in fetching the status. + */ + suspend operator fun invoke(userWalletId: UserWalletId): Either> { + return Either.catch { + currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) + }.mapLeft(CurrencyStatusError::DataError) + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt index 446c289c09..4c45c7a714 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt @@ -61,14 +61,13 @@ internal class NFTDetailsModel @Inject constructor( } private fun onSendClick() { - // TODO implement - // router.push( - // AppRoute.NFTSend( - // userWalletId = params.userWalletId, - // nftAsset = params.nftAsset, - // nftCollectionName = params.nftCollectionName, - // ), - // ) + router.push( + AppRoute.NFTSend( + userWalletId = params.userWalletId, + nftAsset = params.nftAsset, + nftCollectionName = params.nftCollectionName, + ), + ) } private fun navigateBack() { diff --git a/features/send-v2/impl/build.gradle.kts b/features/send-v2/impl/build.gradle.kts index 6858492736..9629c4825b 100644 --- a/features/send-v2/impl/build.gradle.kts +++ b/features/send-v2/impl/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { /** Api */ implementation(projects.features.sendV2.api) implementation(projects.features.txhistory.api) + implementation(projects.features.nft.api) /** Libs */ implementation(projects.libs.crypto) @@ -54,6 +55,7 @@ dependencies { implementation(projects.domain.txhistory) implementation(projects.domain.balanceHiding.models) implementation(projects.domain.balanceHiding) + implementation(projects.domain.nft.models) /** Compose libraries */ implementation(deps.compose.foundation) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt index 55f8d409d6..7493c4d073 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt @@ -1,57 +1,34 @@ package com.tangem.features.send.v2.common -import com.tangem.features.send.v2.common.CommonSendRoute +import com.tangem.core.decompose.navigation.Route import kotlinx.serialization.Serializable -<<<<<<<< HEAD:features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt internal sealed class CommonSendRoute : Route { abstract val isEditMode: Boolean @Serializable data object Empty : CommonSendRoute() { -======== -@Serializable -internal sealed class SendRoute : CommonSendRoute { - - @Serializable - data object Empty : SendRoute(), CommonSendRoute.Empty { ->>>>>>>> b110cd6df ([REDACTED_TASK_KEY] NFT Send. Made common routing abstraction):features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt override val isEditMode = false } @Serializable -<<<<<<<< HEAD:features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt data object Confirm : CommonSendRoute() { -======== - data object Confirm : SendRoute(), CommonSendRoute.Confirm { ->>>>>>>> b110cd6df ([REDACTED_TASK_KEY] NFT Send. Made common routing abstraction):features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt override val isEditMode: Boolean = false } @Serializable data class Destination( override val isEditMode: Boolean, -<<<<<<<< HEAD:features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt ) : CommonSendRoute() -======== - ) : SendRoute(), CommonSendRoute.Destination ->>>>>>>> b110cd6df ([REDACTED_TASK_KEY] NFT Send. Made common routing abstraction):features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt @Serializable data class Amount( override val isEditMode: Boolean, -<<<<<<<< HEAD:features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt ) : CommonSendRoute() @Serializable data object Fee : CommonSendRoute() { -======== - ) : SendRoute(), CommonSendRoute.Amount - - @Serializable - data object Fee : SendRoute(), CommonSendRoute.Fee { ->>>>>>>> b110cd6df ([REDACTED_TASK_KEY] NFT Send. Made common routing abstraction):features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt override val isEditMode: Boolean = true } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/TapHelp.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/TapHelp.kt new file mode 100644 index 0000000000..480650ee0e --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/TapHelp.kt @@ -0,0 +1,62 @@ +package com.tangem.features.send.v2.common.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.impl.R +import kotlinx.coroutines.delay + +private const val TAP_HELP_KEY = "TAP_HELP_KEY" +private const val TAP_HELP_ANIMATION_DELAY = 500L + +internal fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modifier) { + item(key = TAP_HELP_KEY) { + var wrappedIsDisplay by remember { mutableStateOf(false) } + + LaunchedEffect(key1 = isDisplay) { + delay(TAP_HELP_ANIMATION_DELAY) + wrappedIsDisplay = isDisplay + } + + if (wrappedIsDisplay) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxWidth() + .animateItem() + .padding(top = TangemTheme.dimens.spacing20), + ) { + val background = TangemTheme.colors.button.secondary + Icon( + painter = painterResource(id = R.drawable.ic_send_hint_shape_12), + tint = TangemTheme.colors.button.secondary, + contentDescription = null, + modifier = Modifier, + ) + Text( + text = stringResourceSafe(id = R.string.send_summary_tap_hint), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(background) + .padding( + horizontal = TangemTheme.dimens.spacing14, + vertical = TangemTheme.dimens.spacing12, + ), + ) + } + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmSentStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmSentStateTransformer.kt index 6eed2edabb..2d1b64e6b7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmSentStateTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmSentStateTransformer.kt @@ -6,8 +6,8 @@ import com.tangem.features.send.v2.send.ui.state.SendUM import com.tangem.utils.transformer.Transformer internal class SendConfirmSentStateTransformer( - val txData: TransactionData.Uncompiled, - val txUrl: String, + private val txData: TransactionData.Uncompiled, + private val txUrl: String, ) : Transformer { override fun transform(prevState: SendUM): SendUM { return prevState.copy( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt index b734399624..bcf7d3283a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt @@ -5,20 +5,15 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.tangem.common.ui.notifications.NotificationUM @@ -26,12 +21,16 @@ import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.components.transactions.TransactionDoneTitle -import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat -import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.common.ui.tapHelp +import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.ui.state.SendUM import com.tangem.features.send.v2.subcomponents.amount.SendAmountBlockComponent import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBlockComponent @@ -39,11 +38,8 @@ import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.notifications import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent import kotlinx.collections.immutable.ImmutableList -import kotlinx.coroutines.delay -private const val TAP_HELP_KEY = "TAP_HELP_KEY" private const val BLOCKS_KEY = "BLOCKS_KEY" -private const val TAP_HELP_ANIMATION_DELAY = 500L @Suppress("LongParameterList") @Composable @@ -148,45 +144,4 @@ private fun LazyListScope.blocks( feeBlockComponent.Content(modifier = Modifier) } } -} - -private fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modifier) { - item(key = TAP_HELP_KEY) { - var wrappedIsDisplay by remember { mutableStateOf(false) } - - LaunchedEffect(key1 = isDisplay) { - delay(TAP_HELP_ANIMATION_DELAY) - wrappedIsDisplay = isDisplay - } - - if (wrappedIsDisplay) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .fillMaxWidth() - .animateItem() - .padding(top = TangemTheme.dimens.spacing20), - ) { - val background = TangemTheme.colors.button.secondary - Icon( - painter = painterResource(id = R.drawable.ic_send_hint_shape_12), - tint = TangemTheme.colors.button.secondary, - contentDescription = null, - modifier = Modifier, - ) - Text( - text = stringResourceSafe(id = R.string.send_summary_tap_hint), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - modifier = Modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(background) - .padding( - horizontal = TangemTheme.dimens.spacing14, - vertical = TangemTheme.dimens.spacing12, - ), - ) - } - } - } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt index 8074966261..095cb06ce3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt @@ -2,84 +2,195 @@ package com.tangem.features.send.v2.sendnft import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.value.ObserveLifecycleMode +import com.arkivanov.decompose.value.subscribe import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.decompose.navigation.inner.InnerRouter +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.nft.component.NFTDetailsBlockComponent import com.tangem.features.send.v2.api.NFTSendComponent +import com.tangem.features.send.v2.common.CommonSendRoute +import com.tangem.features.send.v2.common.ui.SendContent +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent import com.tangem.features.send.v2.sendnft.model.NFTSendModel +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams +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.SendFeeComponentParams import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.launch +import java.math.BigDecimal internal class DefaultNFTSendComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: NFTSendComponent.Params, + private val nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory, ) : NFTSendComponent, AppComponentContext by appComponentContext { - // private val stackNavigation = StackNavigation() - // - // private val innerRouter = InnerRouter( - // stackNavigation = stackNavigation, - // popCallback = { onChildBack() }, - // ) + private val stackNavigation = StackNavigation() - // private val initialRoute = NFTSendRoute.Empty - // private val currentRouteFlow = MutableStateFlow(initialRoute) + private val innerRouter = InnerRouter( + stackNavigation = stackNavigation, + popCallback = { onChildBack() }, + ) - private val model: NFTSendModel = getOrCreateModel(params = params/*, router = innerRouter*/) + private val initialRoute = CommonSendRoute.Empty + private val currentRouteFlow = MutableStateFlow(initialRoute) - // private val childStack = childStack( - // key = "NFTSendInnerStack", - // source = stackNavigation, - // serializer = null, - // initialConfiguration = initialRoute, - // handleBackButton = true, - // childFactory = { configuration, factoryContext -> - // // todo - // ComposableContentComponent { } - // }, - // ) + 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 -> + createChild( + configuration, + childByContext( + componentContext = factoryContext, + router = innerRouter, + ), + ) + }, + ) 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) - // } - // } + childStack.subscribe( + lifecycle = lifecycle, + mode = ObserveLifecycleMode.CREATE_DESTROY, + ) { stack -> + componentScope.launch { + when (val activeComponent = stack.active.instance) { + is NFTSendConfirmComponent -> if (currentRouteFlow.value.isEditMode) { + // analyticsEventHandler.send(SendAnalyticEvents.ConfirmationScreenOpened) + activeComponent.updateState(model.uiState.value) + } + 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() + val stackState by childStack.subscribeAsState() + val state by model.uiState.collectAsStateWithLifecycle() BackHandler(onBack = ::onChildBack) - // TODO + SendContent( + navigationUM = state.navigationUM, + stackState = stackState, + ) } + private fun createChild(route: CommonSendRoute, factoryContext: AppComponentContext) = when (route) { + is CommonSendRoute.Destination -> getDestinationComponent(factoryContext, route) + is CommonSendRoute.Fee -> getFeeComponent(factoryContext) + CommonSendRoute.Confirm -> getConfirmComponent(factoryContext) + else -> getStubComponent() + } + + private fun getDestinationComponent(factoryContext: AppComponentContext, route: CommonSendRoute) = + SendDestinationComponent( + appComponentContext = factoryContext, + params = SendDestinationComponentParams.DestinationParams( + state = model.uiState.value.destinationUM, + currentRoute = currentRouteFlow.filterIsInstance(), + isBalanceHidingFlow = model.isBalanceHiddenFlow, + analyticsCategoryName = "", // todo + userWalletId = params.userWalletId, + cryptoCurrency = model.cryptoCurrency, + callback = model, + onBackClick = ::onChildBack, + onNextClick = { + if (route.isEditMode) { + onChildBack() + } else { + innerRouter.push(CommonSendRoute.Confirm) + } + }, + ), + ) + + private fun getFeeComponent(factoryContext: AppComponentContext): ComposableContentComponent { + val state = model.uiState.value + val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.value + return if (destinationAddress != null) { + SendFeeComponent( + appComponentContext = factoryContext, + params = SendFeeComponentParams.FeeParams( + state = model.uiState.value.feeUM, + currentRoute = currentRouteFlow.filterIsInstance(), + analyticsCategoryName = "", // todo + userWallet = model.userWallet, + cryptoCurrencyStatus = model.cryptoCurrencyStatus, + feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus, + appCurrency = model.appCurrency, + sendAmount = BigDecimal.ZERO, + destinationAddress = destinationAddress, + callback = model, + onNextClick = ::onChildBack, + ), + ) + } else { + getStubComponent() + } + } + + private fun getConfirmComponent(factoryContext: AppComponentContext) = NFTSendConfirmComponent( + appComponentContext = factoryContext, + nftDetailsBlockComponentFactory = nftDetailsBlockComponentFactory, + params = NFTSendConfirmComponent.Params( + state = model.uiState.value, + analyticsCategoryName = "", // todo + userWallet = model.userWallet, + nftAsset = params.nftAsset, + nftCollectionName = params.nftCollectionName, + cryptoCurrencyStatus = model.cryptoCurrencyStatus, + feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus, + appCurrency = model.appCurrency, + callback = model, + currentRoute = currentRouteFlow.filterIsInstance(), + isBalanceHidingFlow = model.isBalanceHiddenFlow, + ), + ) + + private fun getStubComponent() = ComposableContentComponent { } + 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() - // } + val isEmptyRoute = childStack.value.active.configuration == CommonSendRoute.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 diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/NFTSendRoute.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/NFTSendRoute.kt deleted file mode 100644 index ccec86fa65..0000000000 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/NFTSendRoute.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.features.send.v2.sendnft - -import com.tangem.features.send.v2.common.CommonSendRoute -import kotlinx.serialization.Serializable - -@Serializable -internal sealed class NFTSendRoute : CommonSendRoute { - - @Serializable - data object Empty : NFTSendRoute(), CommonSendRoute.Empty { - override val isEditMode = false - } - - @Serializable - data object Confirm : NFTSendRoute(), CommonSendRoute.Confirm { - override val isEditMode: Boolean = false - } - - @Serializable - data class Destination( - override val isEditMode: Boolean, - ) : NFTSendRoute(), CommonSendRoute.Destination - - @Serializable - data object Fee : NFTSendRoute(), CommonSendRoute.Fee { - override val isEditMode: Boolean = true - } -} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt new file mode 100644 index 0000000000..ead66f202b --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt @@ -0,0 +1,147 @@ +package com.tangem.features.send.v2.sendnft.confirm + +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.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.common.CommonSendRoute +import com.tangem.features.send.v2.common.PredefinedValues +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.sendnft.confirm.model.NFTSendConfirmModel +import com.tangem.features.send.v2.sendnft.confirm.ui.NFTSendConfirmContent +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams +import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent +import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams +import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent +import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData +import kotlinx.coroutines.flow.* +import java.math.BigDecimal + +internal class NFTSendConfirmComponent( + appComponentContext: AppComponentContext, + params: Params, + nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: NFTSendConfirmModel = getOrCreateModel(params = params) + + private val blockClickEnableFlow = MutableStateFlow(false) + + private val destinationBlockComponent = + SendDestinationBlockComponent( + appComponentContext = child("NFTSendConfirmDestinationBlock"), + params = DestinationBlockParams( + state = model.uiState.value.destinationUM, + analyticsCategoryName = params.analyticsCategoryName, + userWalletId = params.userWallet.walletId, + cryptoCurrency = params.cryptoCurrencyStatus.currency, + blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), + predefinedValues = PredefinedValues.Empty, + ), + onResult = model::onDestinationResult, + onClick = model::showEditDestination, + ) + + private val feeBlockComponent = SendFeeBlockComponent( + appComponentContext = child("NFTSendConfirmFeeBlock"), + params = SendFeeComponentParams.FeeBlockParams( + state = model.uiState.value.feeUM, + analyticsCategoryName = params.analyticsCategoryName, + userWallet = params.userWallet, + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, + appCurrency = params.appCurrency, + sendAmount = BigDecimal.ZERO, + destinationAddress = model.confirmData.enteredDestination.orEmpty(), + blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), + ), + onResult = model::onFeeResult, + onClick = model::showEditFee, + ) + + private val nftDetailsBlockComponent = nftDetailsBlockComponentFactory.create( + context = child("NFTDetailsBlock"), + params = NFTDetailsBlockComponent.Params( + userWalletId = params.userWallet.walletId, + nftAsset = params.nftAsset, + nftCollectionName = params.nftCollectionName, + ), + ) + + private val notificationsComponent = NotificationsComponent( + appComponentContext = child("NFTSendConfirmNotifications"), + params = NotificationsComponent.Params( + analyticsCategoryName = params.analyticsCategoryName, + userWalletId = params.userWallet.walletId, + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, + appCurrency = params.appCurrency, + notificationData = NotificationData( + destinationAddress = model.confirmData.enteredDestination.orEmpty(), + memo = model.confirmData.enteredMemo, + amountValue = BigDecimal.ZERO, + reduceAmountBy = BigDecimal.ZERO, + isIgnoreReduce = false, + fee = model.confirmData.fee, + feeError = model.confirmData.feeError, + ), + ), + ) + + init { + model.uiState.onEach { state -> + val confirmUM = state.confirmUM as? ConfirmUM.Content + blockClickEnableFlow.value = confirmUM?.isSending == false + }.launchIn(componentScope) + } + + fun updateState(state: NFTSendUM) { + destinationBlockComponent.updateState(state.destinationUM) + feeBlockComponent.updateState(state.feeUM) + model.updateState(state) + } + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + val notificationState by notificationsComponent.state.collectAsStateWithLifecycle() + + NFTSendConfirmContent( + nftSendUM = state, + destinationBlockComponent = destinationBlockComponent, + feeBlockComponent = feeBlockComponent, + nftDetailsBlockComponent = nftDetailsBlockComponent, + notificationsComponent = notificationsComponent, + notificationsUM = notificationState, + ) + } + + data class Params( + val state: NFTSendUM, + val analyticsCategoryName: String, + val userWallet: UserWallet, + val appCurrency: AppCurrency, + val nftAsset: NFTAsset, + val nftCollectionName: String, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val feeCryptoCurrencyStatus: CryptoCurrencyStatus, + val callback: ModelCallback, + val currentRoute: Flow, + val isBalanceHidingFlow: StateFlow, + ) + + interface ModelCallback { + fun onResult(nftSendUM: NFTSendUM) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/ConfirmData.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/ConfirmData.kt new file mode 100644 index 0000000000..704ce61b5c --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/ConfirmData.kt @@ -0,0 +1,11 @@ +package com.tangem.features.send.v2.sendnft.confirm.model + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.transaction.error.GetFeeError + +data class ConfirmData( + val enteredDestination: String?, + val enteredMemo: String?, + val fee: Fee?, + val feeError: GetFeeError?, +) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmClickIntents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmClickIntents.kt new file mode 100644 index 0000000000..292d62c6c0 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmClickIntents.kt @@ -0,0 +1,16 @@ +package com.tangem.features.send.v2.sendnft.confirm.model + +internal interface NFTSendConfirmClickIntents { + + fun showEditDestination() + + fun showEditFee() + + fun onSendClick() + + fun onExploreClick() + + fun onShareClick() + + fun onFailedTxEmailClick(errorMessage: String) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt new file mode 100644 index 0000000000..95cc3bb76a --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -0,0 +1,335 @@ +package com.tangem.features.send.v2.sendnft.confirm.model + +import android.os.SystemClock +import arrow.core.getOrElse +import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsEventHandler +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.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase +import com.tangem.domain.settings.NeverShowTapHelpUseCase +import com.tangem.features.send.v2.common.CommonSendRoute +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.common.ui.state.NavigationUM +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents +import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource +import com.tangem.features.send.v2.send.ui.state.ButtonsUM +import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent +import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmInitialStateTransformer +import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmSendingStateTransformer +import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformer +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger +import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.transformer.update +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import java.math.BigDecimal +import javax.inject.Inject + +@Suppress("LongParameterList") +@ModelScoped +internal class NFTSendConfirmModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val appRouter: AppRouter, + private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase, + private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase, + private val notificationsUpdateTrigger: NotificationsUpdateTrigger, + private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger, + private val urlOpener: UrlOpener, + private val shareManager: ShareManager, + private val analyticsEventHandler: AnalyticsEventHandler, +) : Model(), NFTSendConfirmClickIntents { + + private val params: NFTSendConfirmComponent.Params = paramsContainer.require() + + private val cryptoCurrencyStatus = params.cryptoCurrencyStatus + + private val _uiState = MutableStateFlow(params.state) + val uiState = _uiState.asStateFlow() + + private val destinationUM + get() = uiState.value.destinationUM as? DestinationUM.Content + private val feeUM + get() = uiState.value.feeUM as? FeeUM.Content + private val feeSelectorUM + get() = feeUM?.feeSelectorUM as? FeeSelectorUM.Content + + val confirmData: ConfirmData + get() = ConfirmData( + enteredDestination = destinationUM?.addressTextField?.value, + enteredMemo = destinationUM?.memoTextField?.value, + fee = feeSelectorUM?.selectedFee, + feeError = (feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error, + ) + + private var sendIdleTimer: Long = 0L + + init { + configConfirmNavigation() + subscribeOnNotificationsUpdateTrigger() + subscribeOnCheckFeeResultUpdates() + initialState() + } + + fun updateState(nftSendUM: NFTSendUM) { + _uiState.value = nftSendUM + updateConfirmNotifications() + } + + fun onFeeResult(feeUM: FeeUM) { + sendIdleTimer = SystemClock.elapsedRealtime() + _uiState.update { it.copy(feeUM = feeUM) } + updateConfirmNotifications() + } + + fun onDestinationResult(destinationUM: DestinationUM) { + _uiState.update { it.copy(destinationUM = destinationUM) } + updateConfirmNotifications() + } + + override fun showEditDestination() { + modelScope.launch { + neverShowTapHelpUseCase() + _uiState.update { + val confirmUM = it.confirmUM as? ConfirmUM.Content + it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM) + } + // analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address)) + router.push(CommonSendRoute.Destination(isEditMode = true)) + } + } + + override fun showEditFee() { + modelScope.launch { + neverShowTapHelpUseCase() + _uiState.update { + val confirmUM = it.confirmUM as? ConfirmUM.Content + it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM) + } + // analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee)) + router.push(CommonSendRoute.Fee) + } + } + + override fun onSendClick() { + _uiState.update(NFTSendConfirmSendingStateTransformer(isSending = true)) + if (SystemClock.elapsedRealtime() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) { + verifyAndSendTransaction() + } else { + modelScope.launch { + sendFeeCheckReloadTrigger.triggerCheckUpdate() + } + } + } + + override fun onExploreClick() { + val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success ?: return + // analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked) + urlOpener.openUrl(confirmUM.txUrl) + } + + override fun onShareClick() { + val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success ?: return + // analyticsEventHandler.send(SendAnalyticEvents.ShareButtonClicked) + shareManager.shareText(confirmUM.txUrl) + } + + override fun onFailedTxEmailClick(errorMessage: String) { + // TODO() + } + + private fun initialState() { + val confirmUM = uiState.value.confirmUM + val feeUM = uiState.value.feeUM + + modelScope.launch { + val isShowTapHelp = isSendTapHelpEnabledUseCase().getOrElse { false } + if (confirmUM is ConfirmUM.Empty || feeUM is FeeUM.Empty) { + _uiState.update { + it.copy( + confirmUM = NFTSendConfirmInitialStateTransformer( + isShowTapHelp = isShowTapHelp, + ).transform(uiState.value.confirmUM), + ) + } + updateConfirmNotifications() + } + } + } + + private fun subscribeOnNotificationsUpdateTrigger() { + notificationsUpdateTrigger.hasErrorFlow + .onEach { hasError -> + _uiState.update { + val feeUM = it.feeUM as? FeeUM.Content + val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content + it.copy( + confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( + isPrimaryButtonEnabled = !hasError && feeSelectorUM != null, + ) ?: it.confirmUM, + ) + } + } + .launchIn(modelScope) + } + + private fun verifyAndSendTransaction() { + // TODO: + } + + // private suspend fun sendTransaction(txData: TransactionData.Uncompiled) { + // val result = sendTransactionUseCase( + // txData = txData, + // userWallet = userWallet, + // network = cryptoCurrency.network, + // ) + // + // _uiState.update(NFTSendConfirmSendingStateTransformer(isSending = false)) + // + // result.fold( + // ifLeft = { error -> + // // alertFactory.getSendTransactionErrorState( + // // error = error, + // // popBack = appRouter::pop, + // // onFailedTxEmailClick = ::onFailedTxEmailClick, + // // ) + // analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol)) + // }, + // ifRight = { + // updateTransactionStatus(txData) + // sendBalanceUpdater.scheduleUpdates() + // // sendAnalyticHelper.sendSuccessAnalytics(cryptoCurrency, uiState.value) + // }, + // ) + // } + + private fun subscribeOnCheckFeeResultUpdates() { + sendFeeCheckReloadTrigger.checkReloadResultFlow.onEach { isFeeResultSuccess -> + if (isFeeResultSuccess) { + sendIdleTimer = SystemClock.elapsedRealtime() + _uiState.update(NFTSendConfirmSendingStateTransformer(isSending = true)) + verifyAndSendTransaction() + } else { + _uiState.update(NFTSendConfirmSendingStateTransformer(isSending = false)) + } + }.launchIn(modelScope) + } + + // private suspend fun updateTransactionStatus(txData: TransactionData.Uncompiled) { + // val txUrl = getExplorerTransactionUrlUseCase( + // userWalletId = userWallet.walletId, + // network = cryptoCurrency.network, + // ).getOrElse { "" } + // _uiState.update(NFTSendConfirmSentStateTransformer(txData, txUrl)) + // } + + private fun updateConfirmNotifications() { + modelScope.launch { + notificationsUpdateTrigger.triggerUpdate( + data = NotificationData( + destinationAddress = confirmData.enteredDestination.orEmpty(), + memo = confirmData.enteredMemo, + amountValue = BigDecimal.ZERO, + reduceAmountBy = BigDecimal.ZERO, + isIgnoreReduce = false, + fee = confirmData.fee, + feeError = confirmData.feeError, + ), + ) + } + _uiState.update { + it.copy( + confirmUM = NFTSendConfirmationNotificationsTransformer( + feeUM = uiState.value.feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrencyStatus.currency, + ).transform(uiState.value.confirmUM), + ) + } + } + + private fun configConfirmNavigation() { + combine( + flow = uiState, + flow2 = params.currentRoute, + transform = { state, route -> state to route }, + ).onEach { (state, _) -> + val confirmUM = state.confirmUM + params.callback.onResult( + state.copy( + navigationUM = NavigationUM.Content( + title = resourceReference( + id = R.string.send_summary_title, + formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name), + ), + subtitle = null, + backIconRes = R.drawable.ic_close_24, + backIconClick = { + analyticsEventHandler.send( + SendAnalyticEvents.CloseButtonClicked( + source = SendScreenSource.Confirm, + isFromSummary = true, + isValid = confirmUM.isPrimaryButtonEnabled, + ), + ) + appRouter.pop() + }, + primaryButton = ButtonsUM.PrimaryButtonUM( + text = when (confirmUM) { + is ConfirmUM.Success -> resourceReference(R.string.common_close) + is ConfirmUM.Content -> if (confirmUM.isSending) { + resourceReference(R.string.send_sending) + } else { + resourceReference(R.string.common_send) + } + else -> resourceReference(R.string.common_send) + }, + iconResId = R.drawable.ic_tangem_24.takeIf { confirmUM is ConfirmUM.Content }, + isEnabled = confirmUM.isPrimaryButtonEnabled, + isHapticClick = confirmUM is ConfirmUM.Content && !confirmUM.isSending, + onClick = { + when (confirmUM) { + is ConfirmUM.Success -> appRouter.pop() + is ConfirmUM.Content -> if (confirmUM.isSending) { + return@PrimaryButtonUM + } else { + onSendClick() + } + else -> return@PrimaryButtonUM + } + }, + ), + prevButton = null, + secondaryPairButtonsUM = ButtonsUM.SecondaryPairButtonsUM( + leftText = resourceReference(R.string.common_explore), + leftIconResId = R.drawable.ic_web_24, + onLeftClick = ::onExploreClick, + rightText = resourceReference(R.string.common_share), + rightIconResId = R.drawable.ic_share_24, + onRightClick = ::onShareClick, + ).takeIf { confirmUM is ConfirmUM.Success }, + ), + ), + ) + }.launchIn(modelScope) + } + + private companion object { + const val CHECK_FEE_UPDATE_DELAY = 10_000L + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmSentStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmSentStateTransformer.kt new file mode 100644 index 0000000000..5c613e80e6 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmSentStateTransformer.kt @@ -0,0 +1,20 @@ +package com.tangem.features.send.v2.sendnft.confirm.model + +import com.tangem.blockchain.common.TransactionData +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import com.tangem.utils.transformer.Transformer + +internal class NFTSendConfirmSentStateTransformer( + private val txData: TransactionData.Uncompiled, + private val txUrl: String, +) : Transformer { + override fun transform(prevState: NFTSendUM): NFTSendUM { + return prevState.copy( + confirmUM = ConfirmUM.Success( + transactionDate = txData.date?.timeInMillis ?: System.currentTimeMillis(), + txUrl = txUrl, + ), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt new file mode 100644 index 0000000000..004117bd1d --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt @@ -0,0 +1,19 @@ +package com.tangem.features.send.v2.sendnft.confirm.model.transformers + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf + +internal class NFTSendConfirmInitialStateTransformer( + private val isShowTapHelp: Boolean, +) : Transformer { + override fun transform(prevState: ConfirmUM): ConfirmUM { + return ConfirmUM.Content( + isSending = false, + showTapHelp = isShowTapHelp, + sendingFooter = TextReference.EMPTY, + notifications = persistentListOf(), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmSendingStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmSendingStateTransformer.kt new file mode 100644 index 0000000000..7b96c1a703 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmSendingStateTransformer.kt @@ -0,0 +1,19 @@ +package com.tangem.features.send.v2.sendnft.confirm.model.transformers + +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import com.tangem.utils.transformer.Transformer + +internal class NFTSendConfirmSendingStateTransformer( + val isSending: Boolean, +) : Transformer { + override fun transform(prevState: NFTSendUM): NFTSendUM { + val confirmUM = prevState.confirmUM as? ConfirmUM.Content ?: return prevState + return prevState.copy( + confirmUM = confirmUM.copy( + isPrimaryButtonEnabled = !isSending, + isSending = isSending, + ), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt new file mode 100644 index 0000000000..af93239501 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt @@ -0,0 +1,55 @@ +package com.tangem.features.send.v2.sendnft.confirm.model.transformers + +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents +import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toPersistentList + +internal class NFTSendConfirmationNotificationsTransformer( + private val feeUM: FeeUM, + private val analyticsEventHandler: AnalyticsEventHandler, + private val cryptoCurrency: CryptoCurrency, +) : Transformer { + override fun transform(prevState: ConfirmUM): ConfirmUM { + val state = prevState as? ConfirmUM.Content ?: return prevState + val feeUM = feeUM as? FeeUM.Content ?: return prevState + return state.copy( + notifications = buildList { + addTooHighNotification(feeUM.feeSelectorUM) + addTooLowNotification(feeUM) + }.toPersistentList(), + ) + } + + private fun MutableList.addTooLowNotification(feeUM: FeeUM.Content) { + val feeSelectorUM = feeUM.feeSelectorUM as? FeeSelectorUM.Content ?: return + val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return + val minimumValue = multipleFees.minimum.amount.value ?: return + val customAmount = feeSelectorUM.customValues.firstOrNull() ?: return + val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) + if (feeSelectorUM.selectedType == FeeType.Custom && minimumValue > customValue) { + add(NotificationUM.Warning.FeeTooLow) + analyticsEventHandler.send( + SendAnalyticEvents.NoticeTransactionDelays(cryptoCurrency.symbol), + ) + } + } + + private fun MutableList.addTooHighNotification(feeSelectorUM: FeeSelectorUM) { + if (feeSelectorUM !is FeeSelectorUM.Content) return + + val (isFeeTooHigh, diff) = checkIfFeeTooHigh(feeSelectorUM) + if (isFeeTooHigh) { + add(NotificationUM.Warning.TooHigh(diff)) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt new file mode 100644 index 0000000000..735f3ecaf0 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt @@ -0,0 +1,104 @@ +package com.tangem.features.send.v2.sendnft.confirm.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.transactions.TransactionDoneTitle +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.common.ui.tapHelp +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent +import com.tangem.features.send.v2.subcomponents.notifications +import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent +import kotlinx.collections.immutable.ImmutableList + +private const val BLOCKS_KEY = "BLOCKS_KEY" + +@Suppress("LongParameterList") +@Composable +internal fun NFTSendConfirmContent( + nftSendUM: NFTSendUM, + destinationBlockComponent: SendDestinationBlockComponent, + nftDetailsBlockComponent: NFTDetailsBlockComponent, + feeBlockComponent: SendFeeBlockComponent, + notificationsComponent: NotificationsComponent, + notificationsUM: ImmutableList, +) { + val confirmUM = nftSendUM.confirmUM as? ConfirmUM.Content + + Column { + LazyColumn( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) { + blocks( + nftSendUM = nftSendUM, + destinationBlockComponent = destinationBlockComponent, + nftDetailsBlockComponent = nftDetailsBlockComponent, + feeBlockComponent = feeBlockComponent, + ) + if (confirmUM != null) { + tapHelp(isDisplay = confirmUM.showTapHelp) + with(notificationsComponent) { + content( + state = notificationsUM, + isClickDisabled = confirmUM.isSending, + ) + } + notifications( + notifications = confirmUM.notifications, + isClickDisabled = confirmUM.isSending, + ) + } + } + } +} + +private fun LazyListScope.blocks( + nftSendUM: NFTSendUM, + destinationBlockComponent: SendDestinationBlockComponent, + nftDetailsBlockComponent: NFTDetailsBlockComponent, + feeBlockComponent: SendFeeBlockComponent, +) { + item(key = BLOCKS_KEY) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + AnimatedVisibility( + visible = nftSendUM.confirmUM is ConfirmUM.Success, + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), + ) { + val wrappedConfirmUM = remember(this) { nftSendUM.confirmUM as ConfirmUM.Success } + TransactionDoneTitle( + title = resourceReference(R.string.sent_transaction_sent_title), + subtitle = resourceReference( + R.string.send_date_format, + wrappedList( + wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter), + wrappedConfirmUM.transactionDate.toTimeFormat(), + ), + ), + modifier = Modifier.padding(vertical = 12.dp), + ) + } + destinationBlockComponent.Content(modifier = Modifier) + + nftDetailsBlockComponent.Content(modifier = Modifier) + + feeBlockComponent.Content(modifier = Modifier) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt index cf70962383..3252c7bc71 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt @@ -2,6 +2,7 @@ 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.confirm.model.NFTSendConfirmModel import com.tangem.features.send.v2.sendnft.model.NFTSendModel import dagger.Binds import dagger.Module @@ -17,4 +18,9 @@ internal interface NFTSendModelModule { @IntoMap @ClassKey(NFTSendModel::class) fun provideNFTSendModel(model: NFTSendModel): Model + + @Binds + @IntoMap + @ClassKey(NFTSendConfirmModel::class) + fun provideNFTSendConfirmModel(model: NFTSendConfirmModel): Model } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt index 3d5d6d178a..7b3be9e2b3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -9,6 +9,7 @@ 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.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency @@ -16,8 +17,10 @@ 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.CommonSendRoute import com.tangem.features.send.v2.common.ui.state.NavigationUM import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent 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 @@ -31,7 +34,8 @@ import kotlin.properties.Delegates internal interface SendNFTComponentCallback : SendFeeComponent.ModelCallback, - SendDestinationComponent.ModelCallback + SendDestinationComponent.ModelCallback, + NFTSendConfirmComponent.ModelCallback @Suppress("LongParameterList") @Stable @@ -42,12 +46,14 @@ internal class NFTSendModel @Inject constructor( private val router: Router, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, + private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, ) : Model(), SendNFTComponentCallback { val params: NFTSendComponent.Params = paramsContainer.require() private val userWalletId = params.userWalletId + private val nftAsset = params.nftAsset private val _uiState = MutableStateFlow(initialState()) val uiState = _uiState.asStateFlow() @@ -70,15 +76,16 @@ internal class NFTSendModel @Inject constructor( _uiState.update { it.copy(navigationUM = navigationUM) } } + override fun onResult(nftSendUM: NFTSendUM) { + _uiState.value = nftSendUM + } + 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() { @@ -93,10 +100,10 @@ internal class NFTSendModel @Inject constructor( ifRight = { wallet -> userWallet = wallet - // cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId).getOrNull() - // ?.filterIsInstance() - // ?.firstOrNull { it.network == nftAsset.network } - // ?: return@launch + cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId).getOrNull() + ?.filterIsInstance() + ?.firstOrNull { it.network == nftAsset.network } + ?: return@launch getCurrenciesStatusUpdates( isSingleWalletWithToken = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), @@ -124,7 +131,7 @@ internal class NFTSendModel @Inject constructor( cryptoCurrencyStatus = cryptoStatus, ).getOrNull() ?: cryptoStatus - // router.push(NFTSendRoute.Destination(isEditMode = false)) + router.replaceAll(CommonSendRoute.Destination(isEditMode = false)) }, ifLeft = { // sendConfirmAlertFactory.getGenericErrorState { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt index 5b7cdce04a..bfb3330873 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt @@ -13,7 +13,7 @@ import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM internal class SendFeeComponent( appComponentContext: AppComponentContext, - private val params: SendFeeComponentParams.FeeParams, + params: SendFeeComponentParams.FeeParams, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: SendFeeModel = getOrCreateModel(params = params)