Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-18 17:08:11 +03:00
commit dbb63a396f
448 changed files with 12230 additions and 2075 deletions

View file

@ -1,35 +1,34 @@
package com.tangem.features.send.v2.send
package com.tangem.features.send.v2.common
import com.tangem.core.decompose.navigation.Route
import kotlinx.serialization.Serializable
@Serializable
internal sealed class SendRoute : Route {
internal sealed class CommonSendRoute : Route {
abstract val isEditMode: Boolean
@Serializable
data object Empty : SendRoute() {
data object Empty : CommonSendRoute() {
override val isEditMode = false
}
@Serializable
data object Confirm : SendRoute() {
data object Confirm : CommonSendRoute() {
override val isEditMode: Boolean = false
}
@Serializable
data class Destination(
override val isEditMode: Boolean,
) : SendRoute()
) : CommonSendRoute()
@Serializable
data class Amount(
override val isEditMode: Boolean,
) : SendRoute()
) : CommonSendRoute()
@Serializable
data class Fee(
override val isEditMode: Boolean = true,
) : SendRoute()
data object Fee : CommonSendRoute() {
override val isEditMode: Boolean = true
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.features.send.v2.common
sealed class PredefinedValues {
data object Empty : PredefinedValues()
sealed class Content : PredefinedValues() {
abstract val amount: String
abstract val address: String
abstract val memo: String?
data class Deeplink(
override val amount: String,
override val address: String,
override val memo: String?,
val transactionId: String,
) : Content()
data class QrCode(
override val amount: String,
override val address: String,
override val memo: String?,
) : Content()
}
}

View file

@ -0,0 +1,87 @@
package com.tangem.features.send.v2.common
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.txhistory.TxHistoryFeatureToggles
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
import com.tangem.utils.coroutines.DelayedWork
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.*
@Suppress("LongParameterList")
internal class SendBalanceUpdater @AssistedInject constructor(
private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase,
private val updateDelayedNetworkStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase,
private val txHistoryFeatureToggles: TxHistoryFeatureToggles,
private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter,
@DelayedWork private val coroutineScope: CoroutineScope,
@Assisted private val userWallet: UserWallet,
@Assisted private val cryptoCurrency: CryptoCurrency,
) {
fun scheduleUpdates() {
coroutineScope.launch {
listOf(
// we should update network to find pending tx after 1 sec
async {
fetchPendingTransactionsUseCase(
userWalletId = userWallet.walletId,
networks = setOf(cryptoCurrency.network),
)
},
// we should update tx history and network for new balances
async {
updateTxHistory()
},
async {
updateNetworkStatuses()
},
).awaitAll()
}
}
private suspend fun updateNetworkStatuses(delay: Long = BALANCE_UPDATE_DELAY) {
updateDelayedNetworkStatusUseCase(
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,
delayMillis = delay,
refresh = true,
)
}
private suspend fun updateTxHistory() {
delay(BALANCE_UPDATE_DELAY)
val txHistoryItemsCountEither = getTxHistoryItemsCountUseCase(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
)
txHistoryItemsCountEither.onRight {
if (txHistoryFeatureToggles.isFeatureEnabled) {
txHistoryContentUpdateEmitter.triggerUpdate()
} else {
getTxHistoryItemsUseCase(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
refresh = true,
)
}
}
}
private companion object {
const val BALANCE_UPDATE_DELAY = 11_000L
}
@AssistedFactory
interface Factory {
fun create(cryptoCurrency: CryptoCurrency, userWallet: UserWallet): SendBalanceUpdater
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.features.send.v2.common
import com.tangem.features.send.v2.common.ui.state.NavigationUM
internal interface SendNavigationModelCallback {
fun onNavigationResult(navigationUM: NavigationUM)
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.v2.send.ui
package com.tangem.features.send.v2.common.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@ -13,12 +13,14 @@ import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.ui.state.NavigationUM
@Composable
internal fun SendContent(state: SendUM, stackState: ChildStack<SendRoute, ComposableContentComponent>) {
internal fun SendContent(
navigationUM: NavigationUM,
stackState: ChildStack<CommonSendRoute, ComposableContentComponent>,
) {
Column(
modifier = Modifier.Companion
.background(color = TangemTheme.colors.background.tertiary)
@ -27,7 +29,7 @@ internal fun SendContent(state: SendUM, stackState: ChildStack<SendRoute, Compos
.systemBarsPadding(),
horizontalAlignment = Alignment.Companion.CenterHorizontally,
) {
SendAppBar(navigationUM = state.navigationUM)
SendAppBar(navigationUM = navigationUM)
Children(
stack = stackState,
animation = stackAnimation(slide()),
@ -35,7 +37,7 @@ internal fun SendContent(state: SendUM, stackState: ChildStack<SendRoute, Compos
) {
it.instance.Content(Modifier.weight(1f))
}
SendNavigationButtons(navigationUM = state.navigationUM)
SendNavigationButtons(navigationUM = navigationUM)
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.v2.send.ui
package com.tangem.features.send.v2.common.ui
import androidx.compose.animation.*
import androidx.compose.foundation.background
@ -29,7 +29,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.common.ui.state.NavigationUM
@Composable
internal fun SendNavigationButtons(navigationUM: NavigationUM, modifier: Modifier = Modifier) {

View file

@ -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,
),
)
}
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.v2.send.confirm.ui.state
package com.tangem.features.send.v2.common.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.notifications.NotificationUM

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.v2.common
package com.tangem.features.send.v2.common.ui.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable

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
@ -16,12 +14,7 @@ import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface SendModelModule {
@Binds
@IntoMap
@ClassKey(SendModel::class)
fun provideSendModel(model: SendModel): Model
internal interface CommonSendModelModule {
@Binds
@IntoMap
@ -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

@ -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

@ -19,11 +19,14 @@ 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.send.v2.api.SendComponent
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.PredefinedValues
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.send.analytics.SendAnalyticEvents
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource
import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.model.SendModel
import com.tangem.features.send.v2.send.ui.SendContent
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent
@ -44,17 +47,17 @@ internal class DefaultSendComponent @AssistedInject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
) : SendComponent, AppComponentContext by appComponentContext {
private val stackNavigation = StackNavigation<SendRoute>()
private val stackNavigation = StackNavigation<CommonSendRoute>()
private val innerRouter = InnerRouter<SendRoute>(
private val innerRouter = InnerRouter<CommonSendRoute>(
stackNavigation = stackNavigation,
popCallback = { onChildBack() },
)
private val initialRoute = if (params.amount == null) {
SendRoute.Destination(isEditMode = false)
CommonSendRoute.Destination(isEditMode = false)
} else {
SendRoute.Empty
CommonSendRoute.Empty
}
private val currentRoute = MutableStateFlow(initialRoute)
@ -112,47 +115,74 @@ internal class DefaultSendComponent @AssistedInject constructor(
BackHandler(onBack = ::onChildBack)
SendContent(
state = state,
navigationUM = state.navigationUM,
stackState = stackState,
)
}
private fun createChild(route: SendRoute, factoryContext: AppComponentContext) = when (route) {
SendRoute.Empty -> getStubComponent()
is SendRoute.Destination -> getDestinationComponent(factoryContext, route)
is SendRoute.Amount -> getAmountComponent(factoryContext, route)
is SendRoute.Fee -> getFeeComponent(factoryContext)
SendRoute.Confirm -> getConfirmComponent(factoryContext)
private fun createChild(route: CommonSendRoute, factoryContext: AppComponentContext) = when (route) {
CommonSendRoute.Empty -> getStubComponent()
is CommonSendRoute.Destination -> getDestinationComponent(factoryContext, route)
is CommonSendRoute.Amount -> getAmountComponent(factoryContext, route)
is CommonSendRoute.Fee -> getFeeComponent(factoryContext)
CommonSendRoute.Confirm -> getConfirmComponent(factoryContext)
}
private fun getDestinationComponent(factoryContext: AppComponentContext, route: SendRoute) =
private fun getDestinationComponent(factoryContext: AppComponentContext, route: CommonSendRoute) =
SendDestinationComponent(
appComponentContext = factoryContext,
params = SendDestinationComponentParams.DestinationParams(
state = model.uiState.value.destinationUM,
currentRoute = currentRoute.filterIsInstance<SendRoute.Destination>(),
currentRoute = currentRoute.filterIsInstance<CommonSendRoute.Destination>(),
isBalanceHidingFlow = model.isBalanceHiddenFlow,
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
userWalletId = params.userWalletId,
cryptoCurrency = params.currency,
callback = model,
isEditMode = route.isEditMode,
onBackClick = ::onChildBack,
onNextClick = {
if (route.isEditMode) {
onChildBack()
} else {
innerRouter.push(CommonSendRoute.Amount(isEditMode = false))
}
},
),
)
private fun getAmountComponent(factoryContext: AppComponentContext, route: SendRoute) = SendAmountComponent(
private fun getAmountComponent(factoryContext: AppComponentContext, route: CommonSendRoute) = SendAmountComponent(
appComponentContext = factoryContext,
params = SendAmountComponentParams.AmountParams(
state = model.uiState.value.amountUM,
currentRoute = currentRoute.filterIsInstance<SendRoute.Amount>(),
currentRoute = currentRoute.filterIsInstance<CommonSendRoute.Amount>(),
isBalanceHidingFlow = model.isBalanceHiddenFlow,
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
userWallet = model.userWallet,
appCurrency = model.appCurrency,
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
callback = model,
isEditMode = route.isEditMode,
predefinedAmountValue = model.predefinedAmountValue,
predefinedValues = model.predefinedValues,
onBackClick = {
if (route.isEditMode) {
onChildBack()
} else {
analyticsEventHandler.send(
SendAnalyticEvents.CloseButtonClicked(
source = SendScreenSource.Amount,
isFromSummary = false,
isValid = model.uiState.value.amountUM.isPrimaryButtonEnabled,
),
)
router.pop()
}
},
onNextClick = {
if (route.isEditMode) {
onChildBack()
} else {
innerRouter.push(CommonSendRoute.Confirm)
}
},
),
)
@ -165,7 +195,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
appComponentContext = factoryContext,
params = SendFeeComponentParams.FeeParams(
state = model.uiState.value.feeUM,
currentRoute = currentRoute.filterIsInstance<SendRoute.Fee>(),
currentRoute = currentRoute.filterIsInstance<CommonSendRoute.Fee>(),
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
userWallet = model.userWallet,
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
@ -174,6 +204,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
sendAmount = sendAmount,
destinationAddress = destinationAddress,
callback = model,
onNextClick = ::onChildBack,
),
)
} else {
@ -187,14 +218,14 @@ internal class DefaultSendComponent @AssistedInject constructor(
val predefinedAddress = params.destinationAddress
val predefinedValues =
if (predefinedAmount != null && predefinedTxId != null && predefinedAddress != null) {
SendConfirmComponent.Params.PredefinedValues.Content(
PredefinedValues.Content.Deeplink(
amount = predefinedAmount,
address = predefinedAddress,
tag = params.tag,
memo = params.tag,
transactionId = predefinedTxId,
)
} else {
SendConfirmComponent.Params.PredefinedValues.Empty
PredefinedValues.Empty
}
return SendConfirmComponent(
appComponentContext = factoryContext,
@ -216,7 +247,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
private fun getStubComponent() = ComposableContentComponent { }
private fun onChildBack() {
val isEmptyRoute = childStack.value.active.configuration == SendRoute.Empty
val isEmptyRoute = childStack.value.active.configuration == CommonSendRoute.Empty
val isEmptyStack = childStack.value.backStack.isEmpty()
val isSuccess = model.uiState.value.confirmUM is ConfirmUM.Success

View file

@ -11,18 +11,20 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.PredefinedValues
import com.tangem.features.send.v2.send.confirm.model.SendConfirmModel
import com.tangem.features.send.v2.send.confirm.ui.SendConfirmContent
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
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.amount.SendAmountComponentParams
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBlockComponent
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams
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 com.tangem.utils.extensions.orZero
import kotlinx.coroutines.flow.*
@ -38,15 +40,13 @@ internal class SendConfirmComponent(
private val destinationBlockComponent =
SendDestinationBlockComponent(
appComponentContext = child("sendConfirmDestinationBlock"),
params = SendDestinationComponentParams.DestinationBlockParams(
params = DestinationBlockParams(
state = model.uiState.value.destinationUM,
analyticsCategoryName = params.analyticsCategoryName,
userWalletId = params.userWallet.walletId,
cryptoCurrency = params.cryptoCurrencyStatus.currency,
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content,
predefinedAddressValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.address,
predefinedMemoValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.tag,
predefinedValues = params.predefinedValues,
),
onResult = model::onDestinationResult,
onClick = model::showEditDestination,
@ -61,8 +61,7 @@ internal class SendConfirmComponent(
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
appCurrency = params.appCurrency,
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content,
predefinedAmountValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.amount,
predefinedValues = params.predefinedValues,
),
onResult = model::onAmountResult,
onClick = model::showEditAmount,
@ -77,8 +76,8 @@ internal class SendConfirmComponent(
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
appCurrency = params.appCurrency,
sendAmount = model.enteredAmount.orZero(),
destinationAddress = model.enteredDestination.orEmpty(),
sendAmount = model.confirmData.enteredAmount.orZero(),
destinationAddress = model.confirmData.enteredDestination.orEmpty(),
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
),
onResult = model::onFeeResult,
@ -93,13 +92,15 @@ internal class SendConfirmComponent(
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
appCurrency = params.appCurrency,
destinationAddress = model.enteredDestination.orEmpty(),
memo = model.enteredMemo,
amountValue = model.enteredAmount.orZero(),
reduceAmountBy = model.reduceAmountBy.orZero(),
isIgnoreReduce = model.isIgnoreReduce,
fee = model.fee,
feeError = model.feeError,
notificationData = NotificationData(
destinationAddress = model.confirmData.enteredDestination.orEmpty(),
memo = model.confirmData.enteredMemo,
amountValue = model.confirmData.enteredAmount.orZero(),
reduceAmountBy = model.confirmData.reduceAmountBy.orZero(),
isIgnoreReduce = model.confirmData.isIgnoreReduce,
fee = model.confirmData.fee,
feeError = model.confirmData.feeError,
),
),
)
@ -140,20 +141,10 @@ internal class SendConfirmComponent(
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
val appCurrency: AppCurrency,
val callback: ModelCallback,
val currentRoute: Flow<SendRoute>,
val currentRoute: Flow<CommonSendRoute>,
val isBalanceHidingFlow: StateFlow<Boolean>,
val predefinedValues: PredefinedValues,
) {
sealed class PredefinedValues {
data object Empty : PredefinedValues()
data class Content(
val transactionId: String,
val amount: String,
val address: String,
val tag: String?,
) : PredefinedValues()
}
}
)
interface ModelCallback {
fun onResult(sendUM: SendUM)

View file

@ -0,0 +1,15 @@
package com.tangem.features.send.v2.send.confirm.model
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.transaction.error.GetFeeError
import java.math.BigDecimal
data class ConfirmData(
val enteredAmount: BigDecimal?,
val reduceAmountBy: BigDecimal,
val isIgnoreReduce: Boolean,
val enteredDestination: String?,
val enteredMemo: String?,
val fee: Fee?,
val feeError: GetFeeError?,
)

View file

@ -5,7 +5,6 @@ import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -25,20 +24,17 @@ import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase
import com.tangem.domain.settings.NeverShowTapHelpUseCase
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.SendBalanceUpdater
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.SendRoute
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.analytics.SendAnalyticHelper
@ -47,7 +43,6 @@ import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmIn
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmSendingStateTransformer
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmSentStateTransformer
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmationNotificationsTransformer
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
@ -58,17 +53,13 @@ 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.features.txhistory.TxHistoryFeatureToggles
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.DelayedWork
import com.tangem.utils.extensions.orZero
import com.tangem.utils.extensions.stripZeroPlainString
import com.tangem.utils.transformer.update
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList", "LargeClass")
@ -89,21 +80,15 @@ internal class SendConfirmModel @Inject constructor(
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase,
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase,
private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger,
private val sendFeeCheckReloadListener: SendFeeCheckReloadListener,
private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter,
private val notificationsUpdateTrigger: NotificationsUpdateTrigger,
private val alertFactory: SendConfirmAlertFactory,
private val sendAnalyticHelper: SendAnalyticHelper,
private val txHistoryFeatureToggles: TxHistoryFeatureToggles,
@DelayedWork private val coroutineScope: CoroutineScope,
private val urlOpener: UrlOpener,
private val shareManager: ShareManager,
sendBalanceUpdaterFactory: SendBalanceUpdater.Factory,
) : Model(), SendConfirmClickIntents {
private val params: SendConfirmComponent.Params = paramsContainer.require()
@ -113,6 +98,8 @@ internal class SendConfirmModel @Inject constructor(
private val cryptoCurrencyStatus = params.cryptoCurrencyStatus
private val cryptoCurrency = cryptoCurrencyStatus.currency
private val sendBalanceUpdater = sendBalanceUpdaterFactory.create(cryptoCurrency, userWallet)
private val _uiState = MutableStateFlow(params.state)
val uiState = _uiState.asStateFlow()
@ -125,20 +112,16 @@ internal class SendConfirmModel @Inject constructor(
private val feeSelectorUM
get() = feeUM?.feeSelectorUM as? FeeSelectorUM.Content
val enteredAmount: BigDecimal?
get() = amountState?.amountTextField?.cryptoAmount?.value
val reduceAmountBy: BigDecimal
get() = amountState?.reduceAmountBy.orZero()
val isIgnoreReduce: Boolean
get() = amountState?.isIgnoreReduce == true
val enteredDestination: String?
get() = destinationUM?.addressTextField?.value
val enteredMemo: String?
get() = destinationUM?.memoTextField?.value
val fee: Fee?
get() = feeSelectorUM?.selectedFee
val feeError: GetFeeError?
get() = (feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error
val confirmData: ConfirmData
get() = ConfirmData(
enteredAmount = amountState?.amountTextField?.cryptoAmount?.value,
enteredMemo = destinationUM?.memoTextField?.value,
reduceAmountBy = amountState?.reduceAmountBy.orZero(),
isIgnoreReduce = amountState?.isIgnoreReduce == true,
enteredDestination = destinationUM?.addressTextField?.value,
fee = feeSelectorUM?.selectedFee,
feeError = (feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error,
)
private var sendIdleTimer: Long = 0L
private var isAmountSubtractAvailable = false
@ -183,7 +166,7 @@ internal class SendConfirmModel @Inject constructor(
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address))
router.push(SendRoute.Destination(isEditMode = true))
router.push(CommonSendRoute.Destination(isEditMode = true))
}
}
@ -195,7 +178,7 @@ internal class SendConfirmModel @Inject constructor(
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount))
router.push(SendRoute.Amount(isEditMode = true))
router.push(CommonSendRoute.Amount(isEditMode = true))
}
}
@ -207,7 +190,7 @@ internal class SendConfirmModel @Inject constructor(
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee))
router.push(SendRoute.Fee())
router.push(CommonSendRoute.Fee)
}
}
@ -236,15 +219,15 @@ internal class SendConfirmModel @Inject constructor(
override fun onFailedTxEmailClick(errorMessage: String) {
val amountValue = amountState?.amountTextField?.cryptoAmount?.value
val feeValue = fee?.amount?.value
val feeValue = confirmData.fee?.amount?.value
val receivingAmount = if (amountValue != null && feeValue != null) {
checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isAmountSubtractAvailable,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = enteredAmount.orZero(),
amountValue = confirmData.enteredAmount.orZero(),
feeValue = feeValue,
reduceAmountBy = reduceAmountBy,
reduceAmountBy = confirmData.reduceAmountBy,
)
} else {
null
@ -257,7 +240,7 @@ internal class SendConfirmModel @Inject constructor(
errorMessage = errorMessage,
blockchainId = cryptoCurrency.network.id.value,
derivationPath = cryptoCurrency.network.derivationPath.value,
destinationAddress = enteredDestination.orEmpty(),
destinationAddress = confirmData.enteredDestination.orEmpty(),
tokenSymbol = if (amount?.type is AmountType.Token) {
amount.currencySymbol
} else {
@ -322,7 +305,7 @@ internal class SendConfirmModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = reduceAmountBy.orZero(),
reduceAmountBy = confirmData.reduceAmountBy.orZero(),
)
modelScope.launch {
@ -369,7 +352,7 @@ internal class SendConfirmModel @Inject constructor(
ifRight = {
updateTransactionStatus(txData)
addTokenToWalletIfNeeded()
scheduleUpdates()
sendBalanceUpdater.scheduleUpdates()
sendAnalyticHelper.sendSuccessAnalytics(cryptoCurrency, uiState.value)
},
)
@ -380,7 +363,7 @@ internal class SendConfirmModel @Inject constructor(
val wallets = destinationUM?.wallets ?: return
val receivingUserWallet = wallets
.firstOrNull { it.address == enteredDestination }
.firstOrNull { it.address == confirmData.enteredDestination }
?: return
val userWalletId = receivingUserWallet.userWalletId ?: return
@ -403,49 +386,6 @@ internal class SendConfirmModel @Inject constructor(
_uiState.update(SendConfirmSentStateTransformer(txData, txUrl))
}
private fun scheduleUpdates() {
coroutineScope.launch {
listOf(
// we should update network to find pending tx after 1 sec
async {
fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrency.network))
},
// we should update tx history and network for new balance
async {
updateTxHistory()
},
async {
updateDelayedCurrencyStatusUseCase(
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,
delayMillis = BALANCE_UPDATE_DELAY,
refresh = true,
)
},
).awaitAll()
}
}
private suspend fun updateTxHistory() {
delay(BALANCE_UPDATE_DELAY)
val txHistoryItemsCountEither = getTxHistoryItemsCountUseCase(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
)
txHistoryItemsCountEither.onRight {
if (txHistoryFeatureToggles.isFeatureEnabled) {
txHistoryContentUpdateEmitter.triggerUpdate()
} else {
getTxHistoryItemsUseCase(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
refresh = true,
)
}
}
}
private fun subscribeOnCheckFeeResultUpdates() {
sendFeeCheckReloadListener.checkReloadResultFlow.onEach { isFeeResultSuccess ->
if (isFeeResultSuccess) {
@ -462,13 +402,13 @@ internal class SendConfirmModel @Inject constructor(
modelScope.launch {
notificationsUpdateTrigger.triggerUpdate(
data = NotificationData(
destinationAddress = enteredDestination.orEmpty(),
memo = enteredMemo,
amountValue = enteredAmount.orZero(),
reduceAmountBy = reduceAmountBy.orZero(),
isIgnoreReduce = isIgnoreReduce,
fee = fee,
feeError = feeError,
destinationAddress = confirmData.enteredDestination.orEmpty(),
memo = confirmData.enteredMemo,
amountValue = confirmData.enteredAmount.orZero(),
reduceAmountBy = confirmData.reduceAmountBy.orZero(),
isIgnoreReduce = confirmData.isIgnoreReduce,
fee = confirmData.fee,
feeError = confirmData.feeError,
),
)
_uiState.update {
@ -554,6 +494,5 @@ internal class SendConfirmModel @Inject constructor(
private companion object {
const val CHECK_FEE_UPDATE_DELAY = 10_000L
const val BALANCE_UPDATE_DELAY = 11_000L
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.features.send.v2.send.confirm.model.transformers
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf

View file

@ -1,6 +1,6 @@
package com.tangem.features.send.v2.send.confirm.model.transformers
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.utils.transformer.Transformer

View file

@ -1,13 +1,13 @@
package com.tangem.features.send.v2.send.confirm.model.transformers
import com.tangem.blockchain.common.TransactionData
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
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<SendUM> {
override fun transform(prevState: SendUM): SendUM {
return prevState.copy(

View file

@ -16,7 +16,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
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

View file

@ -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.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.confirm.ui.state.ConfirmUM
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,
),
)
}
}
}
}

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 CommonSendModelModule {
@Binds
@IntoMap
@ClassKey(SendModel::class)
fun provideSendModel(model: SendModel): Model
@Binds
@IntoMap
@ClassKey(SendConfirmModel::class)
fun provideSendConfirmModel(model: SendConfirmModel): Model
}

View file

@ -30,7 +30,10 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.send.SendRoute
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.common.ui.state.NavigationUM
import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
import com.tangem.features.send.v2.send.confirm.model.SendConfirmAlertFactory
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
@ -92,7 +95,7 @@ internal class SendModel @Inject constructor(
var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
var appCurrency: AppCurrency = AppCurrency.Default
var predefinedAmountValue: String? = null
var predefinedValues: PredefinedValues = PredefinedValues.Empty
private var balanceHidingJobHolder = JobHolder()
@ -218,7 +221,7 @@ internal class SendModel @Inject constructor(
feeCryptoCurrencyStatus = feeCurrencyStatus
if (params.amount != null) {
router.replaceAll(SendRoute.Confirm)
router.replaceAll(CommonSendRoute.Confirm)
}
}
@ -233,10 +236,15 @@ internal class SendModel @Inject constructor(
val parsedQrCode = parseQrCodeUseCase(address, cryptoCurrency).getOrNull()
// Decompose component can be active or inactive depending on its state and navigation stack
// If it is in inactive state use parameter to pass value to amount component
predefinedAmountValue = parsedQrCode?.amount?.parseBigDecimal(cryptoCurrency.decimals)
val amount = parsedQrCode?.amount?.parseBigDecimal(cryptoCurrency.decimals).orEmpty()
predefinedValues = PredefinedValues.Content.QrCode(
amount = amount,
address = parsedQrCode?.address.orEmpty(),
memo = parsedQrCode?.memo,
)
// If it is in active state use flow to update value in amount component
modelScope.launch {
predefinedAmountValue?.let { sendAmountUpdateQRTrigger.triggerUpdateAmount(it) }
amount?.let { sendAmountUpdateQRTrigger.triggerUpdateAmount(it) }
}
}

View file

@ -1,8 +1,8 @@
package com.tangem.features.send.v2.send.ui.state
import com.tangem.common.ui.amountScreen.models.AmountState
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.common.ui.state.NavigationUM
import com.tangem.features.send.v2.common.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

View file

@ -0,0 +1,200 @@
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<CommonSendRoute>()
private val innerRouter = InnerRouter<CommonSendRoute>(
stackNavigation = stackNavigation,
popCallback = { onChildBack() },
)
private val initialRoute = CommonSendRoute.Empty
private val currentRouteFlow = MutableStateFlow<CommonSendRoute>(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 ->
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 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()
BackHandler(onBack = ::onChildBack)
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<CommonSendRoute.Destination>(),
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<CommonSendRoute.Fee>(),
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<CommonSendRoute.Confirm>(),
isBalanceHidingFlow = model.isBalanceHiddenFlow,
),
)
private fun getStubComponent() = ComposableContentComponent { }
private fun onChildBack() {
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
interface Factory : NFTSendComponent.Factory {
override fun create(context: AppComponentContext, params: NFTSendComponent.Params): DefaultNFTSendComponent
}
}

View file

@ -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<CommonSendRoute.Confirm>,
val isBalanceHidingFlow: StateFlow<Boolean>,
)
interface ModelCallback {
fun onResult(nftSendUM: NFTSendUM)
}
}

View file

@ -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?,
)

View file

@ -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)
}

View file

@ -0,0 +1,337 @@
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.SendFeeCheckReloadListener
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 sendFeeCheckReloadListener: SendFeeCheckReloadListener,
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() {
sendFeeCheckReloadListener.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
}
}

View file

@ -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<NFTSendUM> {
override fun transform(prevState: NFTSendUM): NFTSendUM {
return prevState.copy(
confirmUM = ConfirmUM.Success(
transactionDate = txData.date?.timeInMillis ?: System.currentTimeMillis(),
txUrl = txUrl,
),
)
}
}

View file

@ -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<ConfirmUM> {
override fun transform(prevState: ConfirmUM): ConfirmUM {
return ConfirmUM.Content(
isSending = false,
showTapHelp = isShowTapHelp,
sendingFooter = TextReference.EMPTY,
notifications = persistentListOf(),
)
}
}

View file

@ -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<NFTSendUM> {
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,
),
)
}
}

View file

@ -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<ConfirmUM> {
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<NotificationUM>.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<NotificationUM>.addTooHighNotification(feeSelectorUM: FeeSelectorUM) {
if (feeSelectorUM !is FeeSelectorUM.Content) return
val (isFeeTooHigh, diff) = checkIfFeeTooHigh(feeSelectorUM)
if (isFeeTooHigh) {
add(NotificationUM.Warning.TooHigh(diff))
}
}
}

View file

@ -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<NotificationUM>,
) {
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)
}
}
}

View file

@ -0,0 +1,26 @@
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
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
@Binds
@IntoMap
@ClassKey(NFTSendConfirmModel::class)
fun provideNFTSendConfirmModel(model: NFTSendConfirmModel): Model
}

View file

@ -0,0 +1,151 @@
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.GetCryptoCurrenciesUseCase
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.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
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,
NFTSendConfirmComponent.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 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()
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 onResult(nftSendUM: NFTSendUM) {
_uiState.value = nftSendUM
}
override fun onDestinationResult(destinationUM: DestinationUM) {
_uiState.update { it.copy(destinationUM = destinationUM) }
}
override fun onFeeResult(feeUM: FeeUM) {
_uiState.update { it.copy(feeUM = feeUM) }
}
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.replaceAll(CommonSendRoute.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.ui.state.NavigationUM
import com.tangem.features.send.v2.common.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,
)

View file

@ -9,6 +9,7 @@ import com.tangem.common.ui.amountScreen.ui.AmountBlock
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.send.v2.common.PredefinedValues
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams.AmountBlockParams
import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel
import kotlinx.coroutines.flow.launchIn
@ -39,7 +40,7 @@ internal class SendAmountBlockComponent(
AmountBlock(
amountState = state,
isClickDisabled = !isClickEnabled,
isEditingDisabled = params.isPredefinedValues,
isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink,
onClick = onClick,
)
}

View file

@ -4,7 +4,8 @@ import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.PredefinedValues
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent.ModelCallback
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@ -16,7 +17,7 @@ internal sealed class SendAmountComponentParams {
abstract val userWallet: UserWallet
abstract val appCurrency: AppCurrency
abstract val cryptoCurrencyStatus: CryptoCurrencyStatus
abstract val predefinedAmountValue: String?
abstract val predefinedValues: PredefinedValues
data class AmountParams(
override val state: AmountState,
@ -24,11 +25,12 @@ internal sealed class SendAmountComponentParams {
override val userWallet: UserWallet,
override val appCurrency: AppCurrency,
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
override val predefinedAmountValue: String?,
val isEditMode: Boolean,
override val predefinedValues: PredefinedValues,
val callback: ModelCallback,
val currentRoute: Flow<SendRoute.Amount>,
val currentRoute: Flow<CommonSendRoute.Amount>,
val isBalanceHidingFlow: StateFlow<Boolean>,
val onBackClick: () -> Unit,
val onNextClick: () -> Unit,
) : SendAmountComponentParams()
data class AmountBlockParams(
@ -37,8 +39,7 @@ internal sealed class SendAmountComponentParams {
override val userWallet: UserWallet,
override val appCurrency: AppCurrency,
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
override val predefinedAmountValue: String?,
override val predefinedValues: PredefinedValues,
val blockClickEnableFlow: StateFlow<Boolean>,
val isPredefinedValues: Boolean,
) : SendAmountComponentParams()
}

View file

@ -1,7 +1,6 @@
package com.tangem.features.send.v2.subcomponents.amount.model
import androidx.compose.runtime.Stable
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.amountScreen.converters.*
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
@ -13,17 +12,14 @@ 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.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.common.PredefinedValues
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.SendRoute
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.subcomponents.amount.SendAmountComponentParams
import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener
@ -46,8 +42,6 @@ import javax.inject.Inject
internal class SendAmountModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val appRouter: AppRouter,
private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase,
private val sendAmountReduceListener: SendAmountReduceListener,
private val feeReloadTrigger: SendFeeReloadTrigger,
@ -109,7 +103,10 @@ internal class SendAmountModel @Inject constructor(
)
}
}
params.predefinedAmountValue?.let(::onAmountValueChange)
val predefinedValues = params.predefinedValues as? PredefinedValues.Content
if (predefinedValues?.amount != null) {
onAmountValueChange(predefinedValues.amount)
}
}
fun updateState(amountUM: AmountState) {
@ -167,11 +164,6 @@ internal class SendAmountModel @Inject constructor(
)
}
saveResult()
if ((params as? SendAmountComponentParams.AmountParams)?.isEditMode == true) {
router.pop()
} else {
router.push(SendRoute.Confirm)
}
}
private fun subscribeOnAmountReduceToTriggerUpdates() {
@ -247,20 +239,7 @@ internal class SendAmountModel @Inject constructor(
} else {
R.drawable.ic_close_24
},
backIconClick = {
if (route.isEditMode) {
router.pop()
} else {
analyticsEventHandler.send(
SendAnalyticEvents.CloseButtonClicked(
source = SendScreenSource.Amount,
isFromSummary = false,
isValid = state.isPrimaryButtonEnabled,
),
)
appRouter.pop()
}
},
backIconClick = { params.onBackClick() },
primaryButton = ButtonsUM.PrimaryButtonUM(
text = if (route.isEditMode) {
resourceReference(R.string.common_continue)
@ -268,7 +247,10 @@ internal class SendAmountModel @Inject constructor(
resourceReference(R.string.common_next)
},
isEnabled = state.isPrimaryButtonEnabled,
onClick = ::onAmountNext,
onClick = {
onAmountNext()
params.onNextClick()
},
),
prevButton = ButtonsUM.PrimaryButtonUM(
text = TextReference.EMPTY,
@ -276,7 +258,7 @@ internal class SendAmountModel @Inject constructor(
isEnabled = true,
onClick = {
saveResult()
router.pop()
params.onBackClick()
},
).takeIf { route.isEditMode.not() },
secondaryPairButtonsUM = null,

View file

@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.send.v2.common.PredefinedValues
import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel
import com.tangem.features.send.v2.subcomponents.destination.ui.DestinationBlock
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
@ -38,7 +39,7 @@ internal class SendDestinationBlockComponent(
DestinationBlock(
destinationUM = state,
isClickDisabled = !isClickEnabled,
isEditingDisabled = params.isPredefinedValues,
isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink,
onClick = onClick,
)
}

View file

@ -2,7 +2,8 @@ package com.tangem.features.send.v2.subcomponents.destination
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.PredefinedValues
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent.ModelCallback
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import kotlinx.coroutines.flow.Flow
@ -21,9 +22,10 @@ internal sealed class SendDestinationComponentParams {
override val cryptoCurrency: CryptoCurrency,
override val userWalletId: UserWalletId,
val isBalanceHidingFlow: StateFlow<Boolean>,
val currentRoute: Flow<SendRoute.Destination>,
val currentRoute: Flow<CommonSendRoute.Destination>,
val callback: ModelCallback,
val isEditMode: Boolean,
val onBackClick: () -> Unit,
val onNextClick: () -> Unit,
) : SendDestinationComponentParams()
data class DestinationBlockParams(
@ -32,8 +34,6 @@ internal sealed class SendDestinationComponentParams {
override val userWalletId: UserWalletId,
override val cryptoCurrency: CryptoCurrency,
val blockClickEnableFlow: StateFlow<Boolean>,
val predefinedAddressValue: String?,
val predefinedMemoValue: String?,
val isPredefinedValues: Boolean,
val predefinedValues: PredefinedValues,
) : SendDestinationComponentParams()
}

View file

@ -3,7 +3,6 @@ package com.tangem.features.send.v2.subcomponents.destination.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
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
@ -22,13 +21,14 @@ import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.common.PredefinedValues
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.SendRoute
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.subcomponents.destination.SendDestinationComponentParams
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams
import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource
import com.tangem.features.send.v2.subcomponents.destination.analytics.SendDestinationAnalyticEvents
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.*
@ -53,7 +53,6 @@ internal class SendDestinationModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val appRouter: AppRouter,
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
@ -85,19 +84,20 @@ internal class SendDestinationModel @Inject constructor(
}
private fun initialState() {
if ((uiState.value as? DestinationUM.Content)?.isInitialized == false) {
if ((uiState.value as? DestinationUM.Content)?.isInitialized == false || uiState.value is DestinationUM.Empty) {
_uiState.update(
SendDestinationInitialStateTransformer(
cryptoCurrency = cryptoCurrency,
isInitialized = true,
),
)
val params = params as? SendDestinationComponentParams.DestinationBlockParams
if (params?.predefinedAddressValue != null) {
val params = params as? DestinationBlockParams
val predefinedValues = params?.predefinedValues as? PredefinedValues.Content.Deeplink
if (predefinedValues?.address != null) {
_uiState.update(
SendDestinationPredefinedStateTransformer(
address = params.predefinedAddressValue,
memo = params.predefinedMemoValue,
address = predefinedValues.address,
memo = predefinedValues.memo,
),
)
}
@ -269,7 +269,8 @@ internal class SendDestinationModel @Inject constructor(
private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean, isValidMemo: Boolean) {
val isRecent = type == EnterAddressSource.RecentAddress
if (isRecent && isValidAddress && isValidMemo) onNextClick()
if (isRecent && isValidAddress && isValidMemo) saveResult()
(params as? SendDestinationComponentParams.DestinationParams)?.onNextClick?.invoke()
}
private fun saveResult() {
@ -277,15 +278,6 @@ internal class SendDestinationModel @Inject constructor(
params.callback.onDestinationResult(uiState.value)
}
private fun onNextClick() {
saveResult()
if ((params as? SendDestinationComponentParams.DestinationParams)?.isEditMode == true) {
router.pop()
} else {
router.push(SendRoute.Amount(isEditMode = false))
}
}
private fun configDestinationNavigation() {
val params = params as? SendDestinationComponentParams.DestinationParams ?: return
combine(
@ -303,9 +295,7 @@ internal class SendDestinationModel @Inject constructor(
R.drawable.ic_close_24
},
backIconClick = {
if (route.isEditMode) {
router.pop()
} else {
if (!route.isEditMode) {
analyticsEventHandler.send(
SendAnalyticEvents.CloseButtonClicked(
source = SendScreenSource.Address,
@ -313,8 +303,8 @@ internal class SendDestinationModel @Inject constructor(
isValid = state.isPrimaryButtonEnabled,
),
)
appRouter.pop()
}
params.onBackClick()
},
additionalIconRes = R.drawable.ic_qrcode_scan_24,
additionalIconClick = ::onQrCodeScanClick,
@ -325,7 +315,10 @@ internal class SendDestinationModel @Inject constructor(
resourceReference(R.string.common_next)
},
isEnabled = state.isPrimaryButtonEnabled,
onClick = ::onNextClick,
onClick = {
saveResult()
params.onNextClick()
},
),
prevButton = null,
secondaryPairButtonsUM = null,

View file

@ -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)

View file

@ -3,7 +3,7 @@ package com.tangem.features.send.v2.subcomponents.fee
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@ -29,8 +29,9 @@ internal sealed class SendFeeComponentParams {
override val appCurrency: AppCurrency,
override val sendAmount: BigDecimal,
override val destinationAddress: String,
val currentRoute: Flow<SendRoute.Fee>,
val currentRoute: Flow<CommonSendRoute.Fee>,
val callback: SendFeeComponent.ModelCallback,
val onNextClick: () -> Unit,
) : SendFeeComponentParams()
data class FeeBlockParams(

View file

@ -6,12 +6,11 @@ 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.url.UrlOpener
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.GetTransferFeeUseCase
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.features.send.v2.common.NavigationUM
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.ui.state.ButtonsUM
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener
@ -40,9 +39,8 @@ import javax.inject.Inject
internal class SendFeeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val getTransferFeeUseCase: GetTransferFeeUseCase,
private val urlOpener: UrlOpener,
private val analyticsEventHandler: AnalyticsEventHandler,
private val sendFeeAlertFactory: SendFeeAlertFactory,
@ -140,10 +138,10 @@ internal class SendFeeModel @Inject constructor(
),
)
navigate()
saveResult()
}
} else {
navigate()
saveResult()
}
}
@ -194,7 +192,7 @@ internal class SendFeeModel @Inject constructor(
if (isShowLoading) {
_uiState.update(SendFeeLoadingTransformer)
}
getFeeUseCase.invoke(
getTransferFeeUseCase.invoke(
amount = amountValue,
destination = destinationAddress,
userWallet = params.userWallet,
@ -224,7 +222,7 @@ internal class SendFeeModel @Inject constructor(
private fun checkLoadFee() {
modelScope.launch {
getFeeUseCase.invoke(
getTransferFeeUseCase.invoke(
amount = params.sendAmount,
destination = params.destinationAddress,
userWallet = params.userWallet,
@ -283,11 +281,6 @@ internal class SendFeeModel @Inject constructor(
)
}
private fun navigate() {
saveResult()
router.pop()
}
private fun configFeeNavigation() {
val params = params as? SendFeeComponentParams.FeeParams ?: return
combine(
@ -300,11 +293,14 @@ internal class SendFeeModel @Inject constructor(
title = resourceReference(R.string.common_fee_selector_title),
subtitle = null,
backIconRes = R.drawable.ic_back_24,
backIconClick = router::pop,
backIconClick = params.onNextClick,
primaryButton = ButtonsUM.PrimaryButtonUM(
text = resourceReference(R.string.common_continue),
isEnabled = state.isPrimaryButtonEnabled,
onClick = ::onNextClick,
onClick = {
onNextClick()
params.onNextClick()
},
),
prevButton = null,
secondaryPairButtonsUM = null,

View file

@ -2,19 +2,17 @@ package com.tangem.features.send.v2.subcomponents.notifications
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.send.v2.subcomponents.notifications
import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData
import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationsModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.StateFlow
import java.math.BigDecimal
internal class NotificationsComponent(
appComponentContext: AppComponentContext,
@ -45,12 +43,6 @@ internal class NotificationsComponent(
val cryptoCurrencyStatus: CryptoCurrencyStatus,
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
val appCurrency: AppCurrency,
val destinationAddress: String,
val memo: String?,
val amountValue: BigDecimal,
val reduceAmountBy: BigDecimal,
val isIgnoreReduce: Boolean,
val fee: Fee?,
val feeError: GetFeeError?,
val notificationData: NotificationData,
)
}

View file

@ -79,13 +79,7 @@ internal class NotificationsModel @Inject constructor(
private val currency = cryptoCurrencyStatus.currency
private val appCurrency = params.appCurrency
private var destinationAddress = params.destinationAddress
private var memo = params.memo
private var amountValue = params.amountValue
private var reduceAmountBy = params.reduceAmountBy
private var isIgnoreReduce = params.isIgnoreReduce
private var fee = params.fee
private var feeError = params.feeError
private var notificationData = params.notificationData
private val _uiState = MutableStateFlow<ImmutableList<NotificationUM>>(persistentListOf())
val uiState = _uiState.asStateFlow()
@ -111,14 +105,7 @@ internal class NotificationsModel @Inject constructor(
}
private suspend fun updateState(data: NotificationData) {
destinationAddress = data.destinationAddress
memo = data.memo
amountValue = data.amountValue
reduceAmountBy = data.reduceAmountBy
isIgnoreReduce = data.isIgnoreReduce
fee = data.fee
feeError = data.feeError
notificationData = data
buildNotifications()
}
@ -127,26 +114,20 @@ internal class NotificationsModel @Inject constructor(
addFeeUnreachableNotification(
tokenStatus = cryptoCurrencyStatus,
coinStatus = feeCryptoCurrencyStatus,
feeError = feeError,
feeError = notificationData.feeError,
onReload = {
modelScope.launch {
sendFeeReloadTrigger.triggerUpdate(
feeData = SendFeeData(
amount = amountValue,
destinationAddress = destinationAddress,
amount = notificationData.amountValue,
destinationAddress = notificationData.destinationAddress,
),
)
}
},
onClick = ::showTokenDetails,
)
addDomainNotifications(
destinationAddress = destinationAddress,
memo = memo,
amountValue = amountValue,
reduceAmountBy = reduceAmountBy,
fee = fee,
)
addDomainNotifications()
}
notificationsUpdateTrigger.callbackHasError(notifications.any { it is NotificationUM.Error })
@ -167,13 +148,7 @@ internal class NotificationsModel @Inject constructor(
}
}
private suspend fun MutableList<NotificationUM>.addDomainNotifications(
destinationAddress: String,
memo: String?,
amountValue: BigDecimal,
reduceAmountBy: BigDecimal,
fee: Fee?,
) {
private suspend fun MutableList<NotificationUM>.addDomainNotifications() = with(notificationData) {
val balance = cryptoCurrencyStatus.value.amount ?: return
val feeValue = fee?.amount?.value ?: return
val isFeeCoverage = checkFeeCoverage(
@ -311,7 +286,7 @@ internal class NotificationsModel @Inject constructor(
) {
val validationError = validateTransactionUseCase(
userWalletId = userWalletId,
amount = amountValue.convertToSdkAmount(cryptoCurrencyStatus.currency),
amount = enteredAmount.convertToSdkAmount(currency),
fee = fee,
memo = memo,
destination = destinationAddress,
@ -359,7 +334,7 @@ internal class NotificationsModel @Inject constructor(
addHighFeeWarningNotification(
enteredAmountValue = enteredAmount,
cryptoCurrencyStatus = cryptoCurrencyStatus,
ignoreAmountReduce = isIgnoreReduce,
ignoreAmountReduce = notificationData.isIgnoreReduce,
onReduceClick = { reduceBy, reduceByDiff, _ ->
modelScope.launch {
sendAmountReduceTrigger.triggerReduceBy(