Updated on 2026-08-14
This commit is contained in:
commit
50fe9e603d
1919 changed files with 94940 additions and 15178 deletions
|
|
@ -1,6 +1,16 @@
|
|||
package com.tangem.features.send
|
||||
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.send.api.SendFeatureToggles
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultSendFeatureToggles @Inject constructor() : SendFeatureToggles
|
||||
internal class DefaultSendFeatureToggles @Inject constructor(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : SendFeatureToggles {
|
||||
|
||||
override val isHighFeeWarningEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.TWI_1367_HIGH_FEE_WARNING_ENABLED,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.send.common
|
||||
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.features.send.api.subcomponents.amount.AmountRoute
|
||||
import com.tangem.features.send.api.subcomponents.destination.DestinationRoute
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -15,7 +16,7 @@ internal sealed class CommonSendRoute : Route {
|
|||
|
||||
@Serializable
|
||||
data object Confirm : CommonSendRoute() {
|
||||
override val isEditMode: Boolean = true
|
||||
override val isEditMode: Boolean = false
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
|
@ -31,5 +32,5 @@ internal sealed class CommonSendRoute : Route {
|
|||
@Serializable
|
||||
data class Amount(
|
||||
override val isEditMode: Boolean,
|
||||
) : CommonSendRoute()
|
||||
) : CommonSendRoute(), AmountRoute
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto
|
|||
import com.tangem.core.ui.format.bigdecimal.fee
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.impl.R
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -1,99 +0,0 @@
|
|||
package com.tangem.features.send.common.ui
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.arkivanov.decompose.extensions.compose.stack.Children
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.tangem.common.ui.footers.SendingText
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.core.ui.components.Fade
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
|
||||
@Composable
|
||||
internal fun SendContent(
|
||||
navigationUM: NavigationUM,
|
||||
confirmUM: ConfirmUM,
|
||||
stackState: ChildStack<CommonSendRoute, ComposableContentComponent>,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.systemBarsPadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SendAppBar(navigationUM = navigationUM)
|
||||
Children(
|
||||
stack = stackState,
|
||||
animation = stackAnimation { child ->
|
||||
when (child.configuration) {
|
||||
is CommonSendRoute.ConfirmSuccess -> fade(minAlpha = 1.0f)
|
||||
is CommonSendRoute.Confirm -> fade()
|
||||
else -> slide()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxHeight()) {
|
||||
it.instance.Content(Modifier.fillMaxSize(1f))
|
||||
|
||||
if (stackState.active.configuration != CommonSendRoute.ConfirmSuccess) {
|
||||
Fade(
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stackState.active.configuration != CommonSendRoute.ConfirmSuccess) {
|
||||
Column {
|
||||
AnimatedVisibility(
|
||||
visible = stackState.active.configuration == CommonSendRoute.Confirm,
|
||||
enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(),
|
||||
exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(),
|
||||
) {
|
||||
SendingText(footerText = (confirmUM as? ConfirmUM.Content)?.sendingFooter ?: TextReference.EMPTY)
|
||||
}
|
||||
NavigationButtonsBlockV2(
|
||||
navigationUM = navigationUM,
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendAppBar(navigationUM: NavigationUM) {
|
||||
val navigationUMContent = navigationUM as? NavigationUM.Content ?: return
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = navigationUMContent.title.resolveReference(),
|
||||
subtitle = navigationUMContent.subtitle?.resolveReference(),
|
||||
onBackClick = navigationUMContent.backIconClick,
|
||||
onIconClick = navigationUMContent.additionalIconClick,
|
||||
backIconRes = navigationUMContent.backIconRes,
|
||||
iconRes = navigationUMContent.additionalIconRes,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.tangem.features.send.common.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.arkivanov.decompose.extensions.compose.stack.Children
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.tangem.core.ui.components.Fade
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
|
||||
/**
|
||||
* Shared pull-based host for the regular Send and NFT Send flows (both over [CommonSendRoute]). Renders the
|
||||
* ACTIVE child's [ComposableModularContentComponent.Title] / [ComposableModularContentComponent.Footer] slots
|
||||
* in place (matching the previous in-place app-bar/footer behavior), while keeping the per-route slide/fade
|
||||
* Decompose [Children] animation for the Content region (and the bottom `Fade` gradient, hidden on
|
||||
* `ConfirmSuccess`, exactly as the previous `SendContent`).
|
||||
*
|
||||
* Each step's `Footer()` owns its own bottom block (Confirm reveals `SendingText`; Success/Empty render
|
||||
* nothing), so the host no longer special-cases routes for the footer.
|
||||
*/
|
||||
@Composable
|
||||
internal fun SendModularContent(stackState: ChildStack<CommonSendRoute, ComposableModularContentComponent>) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.systemBarsPadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
stackState.active.instance.Title()
|
||||
Children(
|
||||
stack = stackState,
|
||||
animation = stackAnimation { child ->
|
||||
when (child.configuration) {
|
||||
is CommonSendRoute.ConfirmSuccess -> fade(minAlpha = 1.0f)
|
||||
is CommonSendRoute.Confirm -> fade()
|
||||
else -> slide()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxHeight()) {
|
||||
it.instance.Content(Modifier.fillMaxSize(1f))
|
||||
|
||||
if (stackState.active.configuration != CommonSendRoute.ConfirmSuccess) {
|
||||
Fade(
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
stackState.active.instance.Footer()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
package com.tangem.features.send.common.utils
|
||||
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.arkivanov.decompose.value.Value
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
|
||||
/**
|
||||
* Workaround to try fix duplicate route crash
|
||||
*/
|
||||
internal fun Router.safeNextClick(
|
||||
currentRoute: CommonSendRoute,
|
||||
nextRoute: CommonSendRoute,
|
||||
childStack: Value<ChildStack<CommonSendRoute, ComposableContentComponent>>,
|
||||
popBack: () -> Unit,
|
||||
) {
|
||||
if (currentRoute.isEditMode) {
|
||||
popBack()
|
||||
} else {
|
||||
val isAlreadyInStack = childStack.value.items.any { it.configuration == nextRoute }
|
||||
if (isAlreadyInStack) {
|
||||
popTo(nextRoute)
|
||||
} else {
|
||||
push(nextRoute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import com.tangem.features.send.api.NFTSendComponent
|
|||
import com.tangem.features.send.api.SendComponent
|
||||
import com.tangem.features.send.api.SendEntryPointComponent
|
||||
import com.tangem.features.send.api.SendFeatureToggles
|
||||
import com.tangem.features.send.api.SendNotificationsComponent
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent
|
||||
import com.tangem.features.send.entrypoint.DefaultSendEntryPointComponent
|
||||
import com.tangem.features.send.send.DefaultSendComponent
|
||||
import com.tangem.features.send.sendnft.DefaultNFTSendComponent
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
|
|||
import com.tangem.features.send.api.SendComponent
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.entry.SendEntryRoute
|
||||
import com.tangem.features.send.subcomponents.amount.SendAmountUpdateTrigger
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountUpdateTrigger
|
||||
import com.tangem.features.swap.v2.api.SendWithSwapComponent
|
||||
import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ 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.ui.extensions.conditional
|
||||
import com.tangem.features.send.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.api.FeeSelectorComponent
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorComponent
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams
|
||||
import com.tangem.features.send.feeselector.model.FeeSelectorBlockModel
|
||||
import com.tangem.features.send.feeselector.ui.FeeSelectorBlockContent
|
||||
import com.tangem.utils.extensions.isSingleItem
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ 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.send.api.FeeSelectorComponent
|
||||
import com.tangem.features.send.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorComponent
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams
|
||||
import com.tangem.features.send.feeselector.component.FeeSelectorComponentParams
|
||||
import com.tangem.features.send.feeselector.component.extended.FeeExtendedSelectorComponent
|
||||
import com.tangem.features.send.feeselector.component.speed.FeeSpeedSelectorComponent
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.send.feeselector.component
|
||||
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams
|
||||
import com.tangem.features.send.feeselector.model.FeeSelectorIntents
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package com.tangem.features.send.feeselector.component.extended.entity
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
|
||||
@Immutable
|
||||
data class FeeExtendedSelectorUM(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ 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.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.feeselector.component.FeeSelectorComponentParams
|
||||
import com.tangem.features.send.feeselector.component.extended.entity.FeeExtendedSelectorUM
|
||||
import com.tangem.features.send.feeselector.route.FeeSelectorRoute
|
||||
|
|
|
|||
|
|
@ -33,11 +33,11 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.features.send.api.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.entity.FeeFiatRateUM
|
||||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.entity.FeeNonce
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeFiatRateUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.feeselector.component.extended.entity.FeeExtendedSelectorUM
|
||||
import com.tangem.features.send.feeselector.component.speed.ui.RegularFeeItemContent
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
|
|||
|
|
@ -7,7 +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.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.feeselector.component.FeeSelectorComponentParams
|
||||
import com.tangem.features.send.feeselector.component.speed.model.FeeSpeedSelectorModel
|
||||
import com.tangem.features.send.feeselector.component.speed.ui.FeeSpeedSelectorContent
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ 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.navigation.url.UrlOpener
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.feeselector.component.FeeSelectorComponentParams
|
||||
import com.tangem.features.send.feeselector.component.speed.FeeSpeedSelectorIntents
|
||||
import com.tangem.features.send.feeselector.model.FeeSelectorIntents
|
||||
|
|
|
|||
|
|
@ -54,12 +54,12 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.features.send.api.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.api.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.entity.FeeFiatRateUM
|
||||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.entity.FeeNonce
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeFiatRateUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.feeselector.component.speed.FeeSpeedSelectorIntents
|
||||
import com.tangem.features.send.feeselector.component.speed.StubFeeSpeedSelectorIntents
|
||||
import com.tangem.features.send.impl.R
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.send.feeselector.component.token.entity
|
||||
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class FeeTokenSelectorUM(
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import com.tangem.core.navigation.url.UrlOpener
|
|||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.feeselector.component.FeeSelectorComponentParams
|
||||
import com.tangem.features.send.feeselector.component.token.FeeTokenSelectorIntents
|
||||
import com.tangem.features.send.feeselector.component.token.entity.FeeTokenItemState
|
||||
|
|
|
|||
|
|
@ -36,11 +36,11 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.features.send.api.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.entity.FeeFiatRateUM
|
||||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.entity.FeeNonce
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeFiatRateUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.feeselector.component.token.FeeTokenSelectorIntents
|
||||
import com.tangem.features.send.feeselector.component.token.StubFeeTokenSelectorIntents
|
||||
import com.tangem.features.send.feeselector.component.token.entity.FeeTokenItemState
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.send.feeselector.di
|
||||
|
||||
import com.tangem.features.send.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.api.FeeSelectorComponent
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorComponent
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorCheckReloadListener
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorCheckReloadTrigger
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorReloadListener
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils
|
||||
import com.tangem.features.send.impl.R
|
||||
import java.math.BigDecimal
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ import com.tangem.core.navigation.url.UrlOpener
|
|||
import com.tangem.domain.settings.NeverShowTapHelpUseCase
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents.SendScreenSource
|
||||
import com.tangem.features.send.api.callbacks.FeeSelectorModelCallback
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.callbacks.FeeSelectorModelCallback
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.features.send.feeselector.model
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
|
||||
@Stable
|
||||
internal interface FeeSelectorIntents {
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ import com.tangem.domain.transaction.usecase.gasless.GetAvailableFeeTokensUseCas
|
|||
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents.NonceInserted
|
||||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.entity.FeeNonce
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorCheckReloadListener
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorCheckReloadTrigger
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorReloadListener
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import com.tangem.core.decompose.model.ParamsContainer
|
|||
import com.tangem.core.decompose.utils.stack
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams
|
||||
import com.tangem.features.send.feeselector.route.FeeSelectorRoute
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.isSingleItem
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import com.tangem.blockchain.common.transaction.Fee
|
|||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams
|
||||
import com.tangem.features.send.feeselector.model.FeeSelectorIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.send.feeselector.model.transformers
|
||||
|
||||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class FeeItemSelectedTransformer(private val selectedFeeItem: FeeItem) : Transformer<FeeSelectorUM> {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import com.tangem.blockchain.common.transaction.TransactionFee
|
|||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.api.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.feeselector.model.FeeSelectorIntents
|
||||
import com.tangem.features.send.subcomponents.fee.model.converters.custom.bitcoin.BitcoinCustomFeeConverter
|
||||
import com.tangem.features.send.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package com.tangem.features.send.feeselector.model.transformers
|
|||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.feeselector.model.FeeSelectorIntents
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.send.feeselector.model.transformers
|
||||
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class FeeSelectorErrorTransformer(private val error: GetFeeError) : Transformer<FeeSelectorUM> {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import com.tangem.blockchain.common.transaction.Fee
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.api.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.entity.FeeFiatRateUM
|
||||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.entity.FeeNonce
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeFiatRateUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams
|
||||
import com.tangem.features.send.feeselector.model.FeeSelectorIntents
|
||||
import com.tangem.features.send.feeselector.model.FeeSelectorLogic
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isTron
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.send.feeselector.model.transformers
|
||||
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal object FeeSelectorLoadingTransformer : Transformer<FeeSelectorUM> {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.send.feeselector.model.transformers
|
||||
|
||||
import com.tangem.features.send.api.entity.FeeNonce
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class FeeSelectorNonceChangeTransformer(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.send.feeselector.model.transformers
|
||||
|
||||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.features.send.feeselector.model.transformers
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
|
|
|||
|
|
@ -46,11 +46,11 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.features.send.api.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.entity.FeeFiatRateUM
|
||||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.entity.FeeNonce
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeFiatRateUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.utils.extensions.isSingleItem
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWi
|
|||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams
|
||||
import com.tangem.features.send.feeselector.model.FeeSelectorIntents
|
||||
import com.tangem.features.send.feeselector.route.FeeSelectorRoute
|
||||
import com.tangem.features.send.impl.R
|
||||
|
|
|
|||
|
|
@ -1,15 +1,9 @@
|
|||
package com.tangem.features.send.send
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
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
|
||||
|
|
@ -17,45 +11,44 @@ import com.arkivanov.decompose.router.stack.pop
|
|||
import com.arkivanov.decompose.value.ObserveLifecycleMode
|
||||
import com.arkivanov.decompose.value.subscribe
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
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.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.decompose.EmptyComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.models.account.derivationIndex
|
||||
import com.tangem.features.send.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.api.SendComponent
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.subcomponents.amount.AmountRoute
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountComponent
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountComponentParams
|
||||
import com.tangem.features.send.api.subcomponents.destination.DestinationRoute
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponent
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponentParams
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.common.ui.SendContent
|
||||
import com.tangem.features.send.common.ui.SendModularContent
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.send.confirm.SendConfirmComponent
|
||||
import com.tangem.features.send.send.model.SendModel
|
||||
import com.tangem.features.send.send.success.SendConfirmSuccessComponent
|
||||
import com.tangem.features.send.subcomponents.amount.SendAmountComponent
|
||||
import com.tangem.features.send.subcomponents.amount.SendAmountComponentParams
|
||||
import com.tangem.features.send.subcomponents.destination.DefaultSendDestinationBlockComponent
|
||||
import com.tangem.features.send.subcomponents.destination.DefaultSendDestinationComponent
|
||||
import com.tangem.features.send.impl.R
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
|
||||
@Suppress("LargeClass")
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultSendComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: SendComponent.Params,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val amountComponentFactory: SendAmountComponent.Factory,
|
||||
private val destinationComponentFactory: SendDestinationComponent.Factory,
|
||||
private val sendConfirmSuccessComponent: SendConfirmSuccessComponent.Factory,
|
||||
private val feeSelectorComponentFactory: FeeSelectorBlockComponent.Factory,
|
||||
) : SendComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
|
|
@ -106,7 +99,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
type = model.consumeEntryType(),
|
||||
),
|
||||
)
|
||||
if (model.currentRoute.value.isEditMode) {
|
||||
if (childStack.value.active.configuration.isEditMode) {
|
||||
activeComponent.updateState(model.uiState.value)
|
||||
}
|
||||
}
|
||||
|
|
@ -120,7 +113,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
)
|
||||
activeComponent.updateState(model.uiState.value.amountUM)
|
||||
}
|
||||
is DefaultSendDestinationComponent -> {
|
||||
is SendDestinationComponent -> {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.AddressScreenOpened(
|
||||
categoryName = model.analyticCategoryName,
|
||||
|
|
@ -130,41 +123,37 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
activeComponent.updateState(model.uiState.value.destinationUM)
|
||||
}
|
||||
}
|
||||
model.currentRoute.value = stack.active.configuration
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val stackState by childStack.subscribeAsState()
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
BackHandler(
|
||||
onBack = {
|
||||
(state.navigationUM as? NavigationUM.Content)?.backIconClick() ?: onChildBack()
|
||||
},
|
||||
)
|
||||
SendContent(
|
||||
navigationUM = state.navigationUM,
|
||||
confirmUM = state.confirmUM,
|
||||
stackState = stackState,
|
||||
)
|
||||
BackHandler(onBack = ::onChildBack)
|
||||
SendModularContent(stackState = stackState)
|
||||
}
|
||||
|
||||
private fun createChild(route: CommonSendRoute, factoryContext: AppComponentContext) = when (route) {
|
||||
CommonSendRoute.Empty -> getStubComponent()
|
||||
is CommonSendRoute.Destination -> getDestinationComponent(factoryContext)
|
||||
is CommonSendRoute.Amount -> getAmountComponent(factoryContext)
|
||||
private fun createChild(
|
||||
route: CommonSendRoute,
|
||||
factoryContext: AppComponentContext,
|
||||
): ComposableModularContentComponent = when (route) {
|
||||
CommonSendRoute.Empty -> ComposableModularContentComponent.EMPTY
|
||||
is CommonSendRoute.Destination -> getDestinationComponent(route, factoryContext)
|
||||
is CommonSendRoute.Amount -> getAmountComponent(route, factoryContext)
|
||||
is CommonSendRoute.Confirm -> getConfirmComponent(factoryContext)
|
||||
is CommonSendRoute.ConfirmSuccess -> getConfirmSuccessComponent(factoryContext)
|
||||
}
|
||||
|
||||
private fun getDestinationComponent(factoryContext: AppComponentContext): DefaultSendDestinationComponent =
|
||||
DefaultSendDestinationComponent(
|
||||
appComponentContext = factoryContext,
|
||||
private fun getDestinationComponent(
|
||||
route: DestinationRoute,
|
||||
factoryContext: AppComponentContext,
|
||||
): ComposableModularContentComponent {
|
||||
return destinationComponentFactory.create(
|
||||
context = factoryContext,
|
||||
params = SendDestinationComponentParams.DestinationParams(
|
||||
state = model.uiState.value.destinationUM,
|
||||
currentRoute = model.currentRoute.filterIsInstance<CommonSendRoute.Destination>(),
|
||||
route = route,
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
analyticsCategoryName = model.analyticCategoryName,
|
||||
analyticsSendSource = model.analyticsSendSource,
|
||||
|
|
@ -174,13 +163,17 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
callback = model,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAmountComponent(factoryContext: AppComponentContext): ComposableContentComponent {
|
||||
return SendAmountComponent(
|
||||
appComponentContext = factoryContext,
|
||||
private fun getAmountComponent(
|
||||
route: AmountRoute,
|
||||
factoryContext: AppComponentContext,
|
||||
): ComposableModularContentComponent {
|
||||
return amountComponentFactory.create(
|
||||
context = factoryContext,
|
||||
params = SendAmountComponentParams.AmountParams(
|
||||
state = model.uiState.value.amountUM,
|
||||
currentRoute = model.currentRoute.asStateFlow(),
|
||||
route = route,
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
analyticsCategoryName = model.analyticCategoryName,
|
||||
appCurrency = model.appCurrency,
|
||||
|
|
@ -196,7 +189,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getConfirmComponent(factoryContext: AppComponentContext): ComposableContentComponent {
|
||||
private fun getConfirmComponent(factoryContext: AppComponentContext): ComposableModularContentComponent {
|
||||
return if (model.isAvailableForSend) {
|
||||
val cryptoCurrencyStatus = model.cryptoCurrencyStatusFlow.value
|
||||
val feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatusFlow.value
|
||||
|
|
@ -205,7 +198,6 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
params = SendConfirmComponent.Params(
|
||||
state = model.uiState.value,
|
||||
userWallet = model.userWallet,
|
||||
currentRoute = model.currentRoute,
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
analyticsCategoryName = model.analyticCategoryName,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
|
|
@ -228,78 +220,43 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
)
|
||||
} else {
|
||||
model.showAlertError()
|
||||
getStubComponent()
|
||||
ComposableModularContentComponent.EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfirmSuccessComponent(factoryContext: AppComponentContext): ComposableContentComponent {
|
||||
private fun getConfirmSuccessComponent(factoryContext: AppComponentContext): ComposableModularContentComponent {
|
||||
val state = model.uiState.value
|
||||
val sendAmount = (state.amountUM as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
|
||||
val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.value
|
||||
val txUrl = (state.confirmUM as? ConfirmUM.Success)?.txUrl
|
||||
val cryptoCurrencyStatus = model.cryptoCurrencyStatusFlow.value
|
||||
|
||||
if (sendAmount == null ||
|
||||
destinationAddress == null ||
|
||||
txUrl == null
|
||||
) {
|
||||
model.showAlertError()
|
||||
return getStubComponent()
|
||||
return ComposableModularContentComponent.EMPTY
|
||||
}
|
||||
|
||||
val destinationBlockComponent =
|
||||
DefaultSendDestinationBlockComponent(
|
||||
appComponentContext = child("sendConfirmDestinationBlock"),
|
||||
params = SendDestinationComponentParams.DestinationBlockParams(
|
||||
state = model.uiState.value.destinationUM,
|
||||
analyticsCategoryName = model.analyticCategoryName,
|
||||
analyticsSendSource = model.analyticsSendSource,
|
||||
userWalletId = model.userWallet.walletId,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
blockClickEnableFlow = MutableStateFlow(true),
|
||||
predefinedValues = model.predefinedValues,
|
||||
),
|
||||
onResult = { },
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
return SendConfirmSuccessComponent(
|
||||
return sendConfirmSuccessComponent.create(
|
||||
appComponentContext = factoryContext,
|
||||
params = SendConfirmSuccessComponent.Params(
|
||||
sendUMFlow = model.uiState,
|
||||
destinationBlockComponent = destinationBlockComponent,
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = params.currency,
|
||||
predefinedValues = model.predefinedValues,
|
||||
analyticsCategoryName = model.analyticCategoryName,
|
||||
currentRoute = model.currentRoute,
|
||||
txUrl = txUrl,
|
||||
callback = model,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getStubComponent() = StubComponent()
|
||||
|
||||
class StubComponent : ComposableContentComponent {
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
|
||||
color = TangemTheme.colors.icon.primary1,
|
||||
strokeWidth = TangemTheme.dimens.size2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
val isStubComponent = childStack.value.active.instance is StubComponent
|
||||
val isStubComponent = childStack.value.active.instance == EmptyComposableBottomSheetComponent
|
||||
val isSendingInProgress = (model.uiState.value.confirmUM as? ConfirmUM.Content)?.isSending == true
|
||||
|
||||
val isPopSend = isEmptyRoute || isEmptyStack || isSuccess || isStubComponent
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import com.tangem.domain.account.status.usecase.GetAccountCurrencyByAddressUseCa
|
|||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.derivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.send.api.entity.FeeNonce
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationTextFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
|
|
|
|||
|
|
@ -1,35 +1,45 @@
|
|||
package com.tangem.features.send.send.confirm
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import arrow.core.Either
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.footers.SendingText
|
||||
import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton
|
||||
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.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.features.send.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.api.SendNotificationsComponent
|
||||
import com.tangem.features.send.api.SendNotificationsComponent.Params.NotificationData
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountComponentParams
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.send.confirm.model.SendConfirmModel
|
||||
import com.tangem.features.send.send.confirm.ui.SendConfirmContent
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
import com.tangem.features.send.subcomponents.amount.SendAmountBlockComponent
|
||||
import com.tangem.features.send.subcomponents.amount.SendAmountComponentParams
|
||||
import com.tangem.features.send.subcomponents.amount.DefaultSendAmountBlockComponent
|
||||
import com.tangem.features.send.subcomponents.destination.DefaultSendDestinationBlockComponent
|
||||
import com.tangem.features.send.subcomponents.notifications.DefaultSendNotificationsComponent
|
||||
import com.tangem.utils.extensions.orZero
|
||||
|
|
@ -39,7 +49,7 @@ internal class SendConfirmComponent(
|
|||
appComponentContext: AppComponentContext,
|
||||
params: Params,
|
||||
feeSelectorComponentFactory: FeeSelectorBlockComponent.Factory,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SendConfirmModel = getOrCreateModel(params = params)
|
||||
|
||||
|
|
@ -61,7 +71,7 @@ internal class SendConfirmComponent(
|
|||
onClick = model::showEditDestination,
|
||||
)
|
||||
|
||||
private val amountBlockComponent = SendAmountBlockComponent(
|
||||
private val amountBlockComponent = DefaultSendAmountBlockComponent(
|
||||
appComponentContext = child("sendConfirmAmountBlock"),
|
||||
params = SendAmountComponentParams.AmountBlockParams(
|
||||
state = model.uiState.value.amountUM,
|
||||
|
|
@ -107,7 +117,7 @@ internal class SendConfirmComponent(
|
|||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
appCurrency = params.appCurrency,
|
||||
callback = model,
|
||||
notificationData = NotificationData(
|
||||
notificationData = SendNotificationsComponent.Params.NotificationData(
|
||||
destinationAddress = model.confirmData.enteredDestination.orEmpty(),
|
||||
memo = model.confirmData.enteredMemo,
|
||||
amountValue = model.confirmData.enteredAmount.orZero(),
|
||||
|
|
@ -134,6 +144,20 @@ internal class SendConfirmComponent(
|
|||
model.updateState(state)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = stringResourceSafe(R.string.common_send),
|
||||
onBackClick = {
|
||||
model.onBackClick()
|
||||
router.pop()
|
||||
},
|
||||
backIconRes = R.drawable.ic_back_24,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
|
@ -149,6 +173,29 @@ internal class SendConfirmComponent(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
Column {
|
||||
val sendingFooter = (state.confirmUM as? ConfirmUM.Content)?.sendingFooter
|
||||
AnimatedVisibility(
|
||||
visible = sendingFooter != null,
|
||||
enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(),
|
||||
exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(),
|
||||
) {
|
||||
SendingText(footerText = sendingFooter ?: TextReference.EMPTY)
|
||||
}
|
||||
NavigationPrimaryButton(
|
||||
primaryButton = model.primaryButtonUM(state.confirmUM),
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val state: SendUM,
|
||||
val analyticsCategoryName: String,
|
||||
|
|
@ -162,7 +209,6 @@ internal class SendConfirmComponent(
|
|||
val isAccountModeFlow: StateFlow<Boolean>,
|
||||
val appCurrency: AppCurrency,
|
||||
val callback: ModelCallback,
|
||||
val currentRoute: Flow<CommonSendRoute>,
|
||||
val isBalanceHidingFlow: StateFlow<Boolean>,
|
||||
val predefinedValues: PredefinedValues,
|
||||
val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package com.tangem.features.send.send.confirm.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Immutable
|
||||
data class ConfirmData(
|
||||
val enteredAmount: BigDecimal?,
|
||||
val reduceAmountBy: BigDecimal,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import com.tangem.common.routing.AppRouter
|
|||
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
|
@ -34,6 +33,7 @@ import com.tangem.domain.feedback.models.FeedbackEmailType
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.isHotWallet
|
||||
import com.tangem.domain.quotes.IsHighNetworkFeeUseCase
|
||||
import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase
|
||||
import com.tangem.domain.settings.NeverShowTapHelpUseCase
|
||||
import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase
|
||||
|
|
@ -43,24 +43,27 @@ import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
|||
import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.features.send.api.SendNotificationsComponent
|
||||
import com.tangem.features.send.api.SendNotificationsComponent.Params.NotificationData
|
||||
import com.tangem.features.send.api.SendFeatureToggles
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents.SendScreenSource
|
||||
import com.tangem.features.send.api.callbacks.FeeSelectorModelCallback
|
||||
import com.tangem.features.send.api.entity.FeeNonce
|
||||
import com.tangem.features.send.api.params.FeeSelectorParams.FeeStateConfiguration
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountReduceTrigger
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorCheckReloadListener
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorCheckReloadTrigger
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.callbacks.FeeSelectorModelCallback
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams.FeeStateConfiguration
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkAndCalculateSubtractedAmount
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent.Params.NotificationData
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateListener
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateTrigger
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.common.SendBalanceUpdater
|
||||
import com.tangem.features.send.common.SendConfirmAlertFactory
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.send.analytics.SendAnalyticHelper
|
||||
import com.tangem.features.send.send.confirm.SendConfirmComponent
|
||||
import com.tangem.features.send.send.confirm.model.transformers.SendConfirmInitialStateTransformer
|
||||
|
|
@ -68,8 +71,6 @@ import com.tangem.features.send.send.confirm.model.transformers.SendConfirmSendi
|
|||
import com.tangem.features.send.send.confirm.model.transformers.SendConfirmSentStateTransformer
|
||||
import com.tangem.features.send.send.confirm.model.transformers.SendConfirmationNotificationsTransformerV2
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
import com.tangem.features.send.subcomponents.amount.SendAmountReduceTrigger
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import com.tangem.utils.extensions.stripZeroPlainString
|
||||
|
|
@ -79,7 +80,7 @@ import kotlinx.coroutines.flow.*
|
|||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM as FeeSelectorUMRedesigned
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
@Stable
|
||||
|
|
@ -113,6 +114,8 @@ internal class SendConfirmModel @Inject constructor(
|
|||
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
|
||||
private val currenciesRepository: CurrenciesRepository,
|
||||
private val createAndSendGaslessTransactionUseCase: CreateAndSendGaslessTransactionUseCase,
|
||||
private val isHighNetworkFeeUseCase: IsHighNetworkFeeUseCase,
|
||||
private val sendFeatureToggles: SendFeatureToggles,
|
||||
sendBalanceUpdaterFactory: SendBalanceUpdater.Factory,
|
||||
) : Model(), SendConfirmClickIntents, FeeSelectorModelCallback, SendNotificationsComponent.ModelCallback {
|
||||
|
||||
|
|
@ -156,7 +159,6 @@ internal class SendConfirmModel @Inject constructor(
|
|||
|
||||
init {
|
||||
updateAmountSubtractAvailability()
|
||||
configConfirmNavigation()
|
||||
subscribeOnNotificationsUpdateTrigger()
|
||||
subscribeOnCheckFeeResultUpdates()
|
||||
initialState()
|
||||
|
|
@ -299,6 +301,17 @@ internal class SendConfirmModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun onBackClick() {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = analyticsCategoryName,
|
||||
source = SendScreenSource.Confirm,
|
||||
isFromSummary = true,
|
||||
isValid = uiState.value.confirmUM.isPrimaryButtonEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun initialState() {
|
||||
val confirmUM = uiState.value.confirmUM
|
||||
|
||||
|
|
@ -503,6 +516,7 @@ internal class SendConfirmModel @Inject constructor(
|
|||
|
||||
private fun updateConfirmNotifications() {
|
||||
modelScope.launch {
|
||||
val feeCryptoCurrencyStatus = getCurrencyStatusForFeePayment()
|
||||
notificationsUpdateTrigger.triggerUpdate(
|
||||
data = NotificationData(
|
||||
destinationAddress = confirmData.enteredDestination.orEmpty(),
|
||||
|
|
@ -512,9 +526,10 @@ internal class SendConfirmModel @Inject constructor(
|
|||
isIgnoreReduce = confirmData.isIgnoreReduce,
|
||||
fee = confirmData.fee,
|
||||
feeError = confirmData.feeError,
|
||||
feeCryptoCurrencyStatus = getCurrencyStatusForFeePayment(),
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
),
|
||||
)
|
||||
val isHighNetworkFee = isHighNetworkFee(feeCryptoCurrencyStatus.currency)
|
||||
val balance = cryptoCurrencyStatus.value.amount.orZero()
|
||||
val feeValue = confirmData.fee?.amount?.value.orZero()
|
||||
val isTotalSendingMoreThanBalance = confirmData.enteredAmount.orZero() + feeValue > balance
|
||||
|
|
@ -531,6 +546,7 @@ internal class SendConfirmModel @Inject constructor(
|
|||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
appCurrency = appCurrency,
|
||||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
isHighNetworkFee = isHighNetworkFee,
|
||||
isFeeSubtractedFromAmount = isFeeSubtractedFromAmount,
|
||||
isFeeExceedingBalance = isFeeExceedingBalance,
|
||||
).transform(uiState.value.confirmUM),
|
||||
|
|
@ -539,58 +555,13 @@ internal class SendConfirmModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun configConfirmNavigation() {
|
||||
combine(
|
||||
flow = uiState,
|
||||
flow2 = params.currentRoute,
|
||||
transform = { state, route -> state to route },
|
||||
).filter {
|
||||
it.second is CommonSendRoute.Confirm
|
||||
}.onEach { (state, _) ->
|
||||
val confirmUM = state.confirmUM
|
||||
params.callback.onResult(
|
||||
state.copy(
|
||||
navigationUM = NavigationUM.Content(
|
||||
source = CommonSendRoute.Confirm.javaClass.simpleName,
|
||||
title = resourceReference(id = R.string.common_send),
|
||||
subtitle = null,
|
||||
backIconRes = when (confirmUM) {
|
||||
is ConfirmUM.Success -> R.drawable.ic_close_24
|
||||
else -> R.drawable.ic_back_24
|
||||
},
|
||||
backIconClick = {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = analyticsCategoryName,
|
||||
source = SendScreenSource.Confirm,
|
||||
isFromSummary = true,
|
||||
isValid = confirmUM.isPrimaryButtonEnabled,
|
||||
),
|
||||
)
|
||||
router.pop()
|
||||
},
|
||||
primaryButton = primaryButtonUM(),
|
||||
prevButton = null,
|
||||
secondaryPairButtonsUM = (
|
||||
NavigationButton(
|
||||
textReference = resourceReference(R.string.common_explore),
|
||||
iconRes = R.drawable.ic_web_24,
|
||||
onClick = ::onExploreClick,
|
||||
) to NavigationButton(
|
||||
textReference = resourceReference(R.string.common_share),
|
||||
iconRes = R.drawable.ic_share_24,
|
||||
onClick = ::onShareClick,
|
||||
)
|
||||
).takeIf { confirmUM is ConfirmUM.Success },
|
||||
),
|
||||
),
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
private suspend fun isHighNetworkFee(feeCurrency: CryptoCurrency): Boolean {
|
||||
if (!sendFeatureToggles.isHighFeeWarningEnabled) return false
|
||||
val feeAmount = confirmData.fee?.amount?.value ?: return false
|
||||
return isHighNetworkFeeUseCase(feeCurrency, feeAmount)
|
||||
}
|
||||
|
||||
private fun primaryButtonUM(): NavigationButton {
|
||||
val confirmUM = uiState.value.confirmUM
|
||||
fun primaryButtonUM(confirmUM: ConfirmUM): NavigationButton {
|
||||
val isContent = confirmUM is ConfirmUM.Content
|
||||
val isReadyToSend = isContent && !confirmUM.isSending
|
||||
val isHoldToConfirm = userWallet.isHotWallet && isContent
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import com.tangem.core.ui.format.bigdecimal.format
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils
|
||||
import com.tangem.features.send.api.utils.formatFooterFiatFee
|
||||
import com.tangem.features.send.api.utils.getTronTokenFeeSendingText
|
||||
|
|
@ -31,6 +31,7 @@ internal class SendConfirmationNotificationsTransformerV2(
|
|||
private val cryptoCurrency: CryptoCurrency,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val analyticsCategoryName: String,
|
||||
private val isHighNetworkFee: Boolean = false,
|
||||
private val isFeeSubtractedFromAmount: Boolean,
|
||||
private val isFeeExceedingBalance: Boolean,
|
||||
) : Transformer<ConfirmUM> {
|
||||
|
|
@ -42,10 +43,17 @@ internal class SendConfirmationNotificationsTransformerV2(
|
|||
notifications = buildList {
|
||||
addTooHighNotification(feeSelectorUM)
|
||||
addTooLowNotification(feeSelectorUM)
|
||||
addHighNetworkFeeNotification()
|
||||
}.toPersistentList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addHighNetworkFeeNotification() {
|
||||
if (isHighNetworkFee) {
|
||||
add(NotificationUM.Warning.HighNetworkFee)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addTooLowNotification(feeSelectorUM: FeeSelectorUM.Content) {
|
||||
if (FeeCalculationUtils.checkIfCustomFeeTooLow(feeSelectorUM)) {
|
||||
add(NotificationUM.Warning.FeeTooLow)
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ import androidx.compose.ui.unit.dp
|
|||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.common.ui.tapHelp
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
import com.tangem.features.send.subcomponents.amount.SendAmountBlockComponent
|
||||
import com.tangem.features.send.subcomponents.amount.DefaultSendAmountBlockComponent
|
||||
import com.tangem.features.send.subcomponents.destination.DefaultSendDestinationBlockComponent
|
||||
import com.tangem.features.send.subcomponents.notifications
|
||||
import com.tangem.features.send.subcomponents.notifications.DefaultSendNotificationsComponent
|
||||
|
|
@ -31,7 +31,7 @@ private const val BLOCKS_KEY = "BLOCKS_KEY"
|
|||
internal fun SendConfirmContent(
|
||||
sendUM: SendUM,
|
||||
destinationBlockComponent: DefaultSendDestinationBlockComponent,
|
||||
amountBlockComponent: SendAmountBlockComponent,
|
||||
amountBlockComponent: DefaultSendAmountBlockComponent,
|
||||
feeSelectorBlockComponent: FeeSelectorBlockComponent,
|
||||
notificationsComponent: DefaultSendNotificationsComponent,
|
||||
notificationsUM: ImmutableList<NotificationUM>,
|
||||
|
|
@ -73,7 +73,7 @@ internal fun SendConfirmContent(
|
|||
|
||||
private fun LazyListScope.blocks(
|
||||
destinationBlockComponent: DefaultSendDestinationBlockComponent,
|
||||
amountBlockComponent: SendAmountBlockComponent,
|
||||
amountBlockComponent: DefaultSendAmountBlockComponent,
|
||||
feeSelectorBlockComponent: FeeSelectorBlockComponent,
|
||||
) {
|
||||
item(key = BLOCKS_KEY) {
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ import arrow.core.left
|
|||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
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.Route
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseBigDecimalOrNull
|
||||
|
|
@ -41,16 +41,18 @@ import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
|||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForGaslessUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.domain.wallets.models.GetUserWalletError
|
||||
import com.tangem.domain.wallets.models.errors.GetUserWalletError
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.send.api.SendComponent
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents.SendScreenSource
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.api.entity.isFromMainScreenQr
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountComponent
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountUpdateTrigger
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponent
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.common.SendConfirmAlertFactory
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
|
|
@ -58,8 +60,6 @@ import com.tangem.features.send.send.analytics.SendAnalyticEvents
|
|||
import com.tangem.features.send.send.confirm.SendConfirmComponent
|
||||
import com.tangem.features.send.send.success.SendConfirmSuccessComponent
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
import com.tangem.features.send.subcomponents.amount.SendAmountComponent
|
||||
import com.tangem.features.send.subcomponents.amount.SendAmountUpdateTrigger
|
||||
import com.tangem.features.send.subcomponents.destination.model.transformers.SendDestinationInitialStateTransformer
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
|
|
@ -123,8 +123,6 @@ internal class SendModel @Inject constructor(
|
|||
CommonSendRoute.Empty
|
||||
}
|
||||
|
||||
val currentRoute = MutableStateFlow(initialRoute)
|
||||
|
||||
val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
|
||||
field = MutableStateFlow(
|
||||
CryptoCurrencyStatus(
|
||||
|
|
@ -149,7 +147,7 @@ internal class SendModel @Inject constructor(
|
|||
return cryptoCurrencyStatus.isAvailableForSend() && feeCryptoCurrencyStatus.isAvailableForSend()
|
||||
}
|
||||
|
||||
val isUnavailableForSend: Boolean
|
||||
private val isUnavailableForSend: Boolean
|
||||
get() {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusFlow.value
|
||||
val feeCryptoCurrencyStatus = feeCryptoCurrencyStatusFlow.value
|
||||
|
|
@ -179,10 +177,6 @@ internal class SendModel @Inject constructor(
|
|||
initAppCurrency()
|
||||
}
|
||||
|
||||
override fun onNavigationResult(navigationUM: NavigationUM) {
|
||||
uiState.update { it.copy(navigationUM = navigationUM) }
|
||||
}
|
||||
|
||||
override fun onDestinationResult(destinationUM: DestinationUM) {
|
||||
uiState.update { it.copy(destinationUM = destinationUM) }
|
||||
}
|
||||
|
|
@ -196,9 +190,9 @@ internal class SendModel @Inject constructor(
|
|||
uiState.update { sendUM }
|
||||
}
|
||||
|
||||
override fun onBackClick() {
|
||||
when (val route = currentRoute.value) {
|
||||
is CommonSendRoute.Amount -> if (!route.isEditMode) {
|
||||
override fun onBackClick(currentRoute: Route) {
|
||||
when (currentRoute) {
|
||||
is CommonSendRoute.Amount -> if (!currentRoute.isEditMode) {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = analyticCategoryName,
|
||||
|
|
@ -208,7 +202,7 @@ internal class SendModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
}
|
||||
is CommonSendRoute.Destination -> if (!route.isEditMode) {
|
||||
is CommonSendRoute.Destination -> if (!currentRoute.isEditMode) {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = analyticCategoryName,
|
||||
|
|
@ -224,11 +218,11 @@ internal class SendModel @Inject constructor(
|
|||
router.pop()
|
||||
}
|
||||
|
||||
override fun onNextClick() {
|
||||
if (currentRoute.value.isEditMode) {
|
||||
onBackClick()
|
||||
override fun onNextClick(currentRoute: Route) {
|
||||
if ((currentRoute as? CommonSendRoute)?.isEditMode == true) {
|
||||
onBackClick(currentRoute)
|
||||
} else {
|
||||
when (currentRoute.value) {
|
||||
when (currentRoute) {
|
||||
is CommonSendRoute.Amount -> {
|
||||
val nextRoute = if (predefinedValues.isFromMainScreenQr) {
|
||||
CommonSendRoute.Confirm
|
||||
|
|
@ -239,7 +233,7 @@ internal class SendModel @Inject constructor(
|
|||
}
|
||||
is CommonSendRoute.Destination -> router.push(CommonSendRoute.Confirm)
|
||||
CommonSendRoute.Confirm -> router.push(CommonSendRoute.ConfirmSuccess)
|
||||
else -> onBackClick()
|
||||
else -> router.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -263,7 +257,6 @@ internal class SendModel @Inject constructor(
|
|||
feeSelectorUM = FeeSelectorUM.Loading,
|
||||
confirmUM = ConfirmUM.Empty,
|
||||
confirmData = null,
|
||||
navigationUM = NavigationUM.Empty,
|
||||
)
|
||||
}
|
||||
router.popTo(CommonSendRoute.Amount(isEditMode = false))
|
||||
|
|
@ -442,7 +435,7 @@ internal class SendModel @Inject constructor(
|
|||
cryptoCurrencyStatusFlow,
|
||||
feeCryptoCurrencyStatusFlow,
|
||||
) { cryptoCurrencyStatus, _ ->
|
||||
if (!isAvailableForSend || currentRoute.value != initialRoute) {
|
||||
if (!isAvailableForSend) {
|
||||
if (isUnavailableForSend) showAlertError()
|
||||
return@combine
|
||||
}
|
||||
|
|
@ -548,7 +541,6 @@ internal class SendModel @Inject constructor(
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
).transform(DestinationUM.Empty()),
|
||||
confirmUM = ConfirmUM.Empty,
|
||||
navigationUM = NavigationUM.Empty,
|
||||
confirmData = null,
|
||||
feeSelectorUM = FeeSelectorUM.Loading,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,27 +1,77 @@
|
|||
package com.tangem.features.send.send.success
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.navigationButtons.DoneButtons
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
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.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationBlockComponent
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponentParams
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.send.success.model.SendConfirmSuccessModel
|
||||
import com.tangem.features.send.send.success.ui.SendConfirmSuccessContent
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
internal class SendConfirmSuccessComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
internal class SendConfirmSuccessComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
destinationBlockComponentFactory: SendDestinationBlockComponent.Factory,
|
||||
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SendConfirmSuccessModel = getOrCreateModel(params = params)
|
||||
private val destinationBlockComponent: SendDestinationBlockComponent = params.destinationBlockComponent
|
||||
private val destinationBlockComponent: SendDestinationBlockComponent = destinationBlockComponentFactory.create(
|
||||
context = child("sendConfirmDestinationBlock"),
|
||||
params = SendDestinationComponentParams.DestinationBlockParams(
|
||||
state = model.uiState.value.destinationUM,
|
||||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.Send,
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
blockClickEnableFlow = MutableStateFlow(true),
|
||||
predefinedValues = params.predefinedValues,
|
||||
isAddContactAvailable = true,
|
||||
),
|
||||
onResult = {},
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
AppBarWithBackButtonAndIcon(
|
||||
onBackClick = {
|
||||
model.onBackClick()
|
||||
router.pop()
|
||||
},
|
||||
backIconRes = R.drawable.ic_close_24,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
|
|
@ -32,11 +82,43 @@ internal class SendConfirmSuccessComponent(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
) {
|
||||
DoneButtons(
|
||||
(NavigationButton(
|
||||
textReference = resourceReference(R.string.common_explore),
|
||||
iconRes = R.drawable.ic_web_24,
|
||||
onClick = model::onExploreClick,
|
||||
) to NavigationButton(
|
||||
textReference = resourceReference(R.string.common_share),
|
||||
iconRes = R.drawable.ic_share_24,
|
||||
onClick = model::onShareClick,
|
||||
)).takeIf { params.txUrl.isNotEmpty() },
|
||||
)
|
||||
PrimaryButton(
|
||||
text = stringResourceSafe(R.string.common_close),
|
||||
onClick = router::pop,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val sendUMFlow: StateFlow<SendUM>,
|
||||
val destinationBlockComponent: SendDestinationBlockComponent,
|
||||
val userWalletId: UserWalletId,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val analyticsCategoryName: String,
|
||||
val currentRoute: Flow<CommonSendRoute>,
|
||||
val predefinedValues: PredefinedValues,
|
||||
val txUrl: String,
|
||||
val callback: ModelCallback,
|
||||
)
|
||||
|
|
@ -44,4 +126,9 @@ internal class SendConfirmSuccessComponent(
|
|||
interface ModelCallback {
|
||||
fun onResult(sendUM: SendUM)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(appComponentContext: AppComponentContext, params: Params): SendConfirmSuccessComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +1,17 @@
|
|||
package com.tangem.features.send.send.success.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
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.navigation.share.ShareManager
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents.SendScreenSource
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.send.success.SendConfirmSuccessComponent
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
|
|
@ -31,7 +20,6 @@ internal class SendConfirmSuccessModel @Inject constructor(
|
|||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val appRouter: AppRouter,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val shareManager: ShareManager,
|
||||
) : Model() {
|
||||
|
|
@ -39,73 +27,23 @@ internal class SendConfirmSuccessModel @Inject constructor(
|
|||
private val _uiState = params.sendUMFlow
|
||||
val uiState = _uiState
|
||||
|
||||
init {
|
||||
configConfirmSuccessNavigation()
|
||||
fun onBackClick() {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
source = SendScreenSource.Confirm,
|
||||
isFromSummary = true,
|
||||
isValid = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun configConfirmSuccessNavigation() {
|
||||
combine(
|
||||
flow = uiState,
|
||||
flow2 = params.currentRoute,
|
||||
transform = { state, route -> state to route },
|
||||
).filter { (state, route) ->
|
||||
// Emit the success navigation exactly once. Building NavigationUM.Content here creates fresh
|
||||
// lambdas every time, so the SendUM written back via callback.onResult is never equal to the
|
||||
// previous one — without this guard the combine re-triggers itself endlessly and the success
|
||||
// screen recomposes forever (never reaching Compose idle). See [REDACTED_TASK_KEY].
|
||||
route is CommonSendRoute.ConfirmSuccess &&
|
||||
(state.navigationUM as? NavigationUM.Content)?.source !=
|
||||
CommonSendRoute.ConfirmSuccess.javaClass.simpleName
|
||||
}.onEach { (state, _) ->
|
||||
params.callback.onResult(
|
||||
state.copy(
|
||||
navigationUM = NavigationUM.Content(
|
||||
source = CommonSendRoute.ConfirmSuccess.javaClass.simpleName,
|
||||
title = stringReference(""),
|
||||
subtitle = null,
|
||||
backIconRes = R.drawable.ic_close_24,
|
||||
backIconClick = {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
source = SendScreenSource.Confirm,
|
||||
isFromSummary = true,
|
||||
isValid = true,
|
||||
),
|
||||
)
|
||||
appRouter.pop()
|
||||
},
|
||||
primaryButton = NavigationButton(
|
||||
textReference = resourceReference(R.string.common_close),
|
||||
iconRes = null,
|
||||
isEnabled = true,
|
||||
isHapticClick = false,
|
||||
onClick = {
|
||||
appRouter.pop()
|
||||
},
|
||||
),
|
||||
prevButton = null,
|
||||
secondaryPairButtonsUM = (NavigationButton(
|
||||
textReference = resourceReference(R.string.common_explore),
|
||||
iconRes = R.drawable.ic_web_24,
|
||||
onClick = ::onExploreClick,
|
||||
) to NavigationButton(
|
||||
textReference = resourceReference(R.string.common_share),
|
||||
iconRes = R.drawable.ic_share_24,
|
||||
onClick = ::onShareClick,
|
||||
)).takeIf { params.txUrl.isNotEmpty() },
|
||||
),
|
||||
),
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onExploreClick() {
|
||||
fun onExploreClick() {
|
||||
analyticsEventHandler.send(CommonSendAnalyticEvents.ExploreButtonClicked(params.analyticsCategoryName))
|
||||
urlOpener.openUrl(params.txUrl)
|
||||
}
|
||||
|
||||
private fun onShareClick() {
|
||||
fun onShareClick() {
|
||||
analyticsEventHandler.send(CommonSendAnalyticEvents.ShareButtonClicked(params.analyticsCategoryName))
|
||||
shareManager.shareText(params.txUrl)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.amountScreen.ui.AmountBlock
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2
|
||||
import com.tangem.core.ui.components.Fade
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
|
||||
|
|
@ -25,8 +24,8 @@ import com.tangem.core.ui.utils.toTimeFormat
|
|||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationBlockComponent
|
||||
import com.tangem.features.send.common.ui.FeeBlockSuccess
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
|
||||
@Composable
|
||||
internal fun SendConfirmSuccessContent(sendUM: SendUM, destinationBlockComponent: SendDestinationBlockComponent) {
|
||||
|
|
@ -63,14 +62,6 @@ internal fun SendConfirmSuccessContent(sendUM: SendUM, destinationBlockComponent
|
|||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
)
|
||||
}
|
||||
NavigationButtonsBlockV2(
|
||||
navigationUM = sendUM.navigationUM,
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
package com.tangem.features.send.send.ui.state
|
||||
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.send.confirm.model.ConfirmData
|
||||
|
||||
|
|
@ -12,6 +11,5 @@ internal data class SendUM(
|
|||
val destinationUM: DestinationUM,
|
||||
val feeSelectorUM: FeeSelectorUM,
|
||||
val confirmUM: ConfirmUM,
|
||||
val navigationUM: NavigationUM,
|
||||
val confirmData: ConfirmData?,
|
||||
)
|
||||
|
|
@ -4,7 +4,6 @@ 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
|
||||
|
|
@ -16,28 +15,28 @@ 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.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.send.api.NFTSendComponent
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.subcomponents.destination.DestinationRoute
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponent
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponentParams
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.common.ui.SendContent
|
||||
import com.tangem.features.send.common.ui.SendModularContent
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.sendnft.confirm.NFTSendConfirmComponent
|
||||
import com.tangem.features.send.sendnft.model.NFTSendModel
|
||||
import com.tangem.features.send.sendnft.success.NFTSendSuccessComponent
|
||||
import com.tangem.features.send.subcomponents.destination.DefaultSendDestinationComponent
|
||||
import com.tangem.features.send.impl.R
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class DefaultNFTSendComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: NFTSendComponent.Params,
|
||||
private val destinationComponentFactory: SendDestinationComponent.Factory,
|
||||
private val nftSendConfirmComponentFactory: NFTSendConfirmComponent.Factory,
|
||||
private val nftSendSuccessComponentFactory: NFTSendSuccessComponent.Factory,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
|
|
@ -76,37 +75,35 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
|
|||
lifecycle = lifecycle,
|
||||
mode = ObserveLifecycleMode.CREATE_DESTROY,
|
||||
) { stack ->
|
||||
componentScope.launch {
|
||||
when (val activeComponent = stack.active.instance) {
|
||||
is NFTSendConfirmComponent -> {
|
||||
val fromCurrency = model.cryptoCurrency
|
||||
val fromDerivationIndex = model.account?.derivationIndex?.value
|
||||
.takeIf { model.isAccountsMode }
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.ConfirmationScreenOpened(
|
||||
categoryName = analyticsCategoryName,
|
||||
source = analyticsSendSource,
|
||||
sendBlockchain = fromCurrency.network.name,
|
||||
sendToken = fromCurrency.symbol,
|
||||
fromDerivationIndex = fromDerivationIndex,
|
||||
toDerivationIndex = null,
|
||||
),
|
||||
)
|
||||
if (model.currentRouteFlow.value.isEditMode) {
|
||||
activeComponent.updateState(model.uiState.value)
|
||||
}
|
||||
}
|
||||
is DefaultSendDestinationComponent -> {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.AddressScreenOpened(
|
||||
categoryName = analyticsCategoryName,
|
||||
source = analyticsSendSource,
|
||||
),
|
||||
)
|
||||
activeComponent.updateState(model.uiState.value.destinationUM)
|
||||
when (val activeComponent = stack.active.instance) {
|
||||
is NFTSendConfirmComponent -> {
|
||||
val fromCurrency = model.cryptoCurrency
|
||||
val fromDerivationIndex = model.account?.derivationIndex?.value
|
||||
.takeIf { model.isAccountsMode }
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.ConfirmationScreenOpened(
|
||||
categoryName = analyticsCategoryName,
|
||||
source = analyticsSendSource,
|
||||
sendBlockchain = fromCurrency.network.name,
|
||||
sendToken = fromCurrency.symbol,
|
||||
fromDerivationIndex = fromDerivationIndex,
|
||||
toDerivationIndex = null,
|
||||
),
|
||||
)
|
||||
// Push current state into a reused Confirm on (re)entry. Confirm.isEditMode is `true`
|
||||
if (stack.active.configuration.isEditMode) {
|
||||
activeComponent.updateState(model.uiState.value)
|
||||
}
|
||||
}
|
||||
model.currentRouteFlow.emit(stack.active.configuration)
|
||||
is SendDestinationComponent -> {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.AddressScreenOpened(
|
||||
categoryName = analyticsCategoryName,
|
||||
source = analyticsSendSource,
|
||||
),
|
||||
)
|
||||
activeComponent.updateState(model.uiState.value.destinationUM)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -114,67 +111,71 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val stackState by childStack.subscribeAsState()
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
BackHandler(onBack = model::onBackClick)
|
||||
SendContent(
|
||||
navigationUM = state.navigationUM,
|
||||
confirmUM = state.confirmUM,
|
||||
stackState = stackState,
|
||||
)
|
||||
BackHandler(onBack = ::onChildBack)
|
||||
SendModularContent(stackState = stackState)
|
||||
}
|
||||
|
||||
private fun createChild(route: CommonSendRoute, factoryContext: AppComponentContext) = when (route) {
|
||||
is CommonSendRoute.Destination -> getDestinationComponent(factoryContext)
|
||||
private fun createChild(
|
||||
route: CommonSendRoute,
|
||||
factoryContext: AppComponentContext,
|
||||
): ComposableModularContentComponent = when (route) {
|
||||
is CommonSendRoute.Destination -> getDestinationComponent(route, factoryContext)
|
||||
CommonSendRoute.Confirm -> getConfirmComponent(factoryContext)
|
||||
CommonSendRoute.ConfirmSuccess -> getSuccessComponent(factoryContext)
|
||||
else -> getStubComponent()
|
||||
// Empty is the bootstrap placeholder until the currency status resolves; NFT has no Amount step.
|
||||
CommonSendRoute.Empty,
|
||||
is CommonSendRoute.Amount,
|
||||
-> ComposableModularContentComponent.EMPTY
|
||||
}
|
||||
|
||||
private fun getDestinationComponent(factoryContext: AppComponentContext): DefaultSendDestinationComponent =
|
||||
DefaultSendDestinationComponent(
|
||||
appComponentContext = factoryContext,
|
||||
params = SendDestinationComponentParams.DestinationParams(
|
||||
state = model.uiState.value.destinationUM,
|
||||
currentRoute = model.currentRouteFlow.filterIsInstance<CommonSendRoute.Destination>(),
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
title = resourceReference(R.string.nft_send),
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
analyticsSendSource = analyticsSendSource,
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = model.cryptoCurrency,
|
||||
callback = model,
|
||||
),
|
||||
)
|
||||
|
||||
private fun getConfirmComponent(factoryContext: AppComponentContext) = nftSendConfirmComponentFactory.create(
|
||||
appComponentContext = factoryContext,
|
||||
params = NFTSendConfirmComponent.Params(
|
||||
state = model.uiState.value,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
userWallet = model.userWallet,
|
||||
nftAsset = params.nftAsset,
|
||||
nftCollectionName = params.nftCollectionName,
|
||||
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
|
||||
feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus,
|
||||
appCurrency = model.appCurrency,
|
||||
callback = model,
|
||||
currentRoute = model.currentRouteFlow.filterIsInstance<CommonSendRoute.Confirm>(),
|
||||
private fun getDestinationComponent(
|
||||
route: DestinationRoute,
|
||||
factoryContext: AppComponentContext,
|
||||
): ComposableModularContentComponent = destinationComponentFactory.create(
|
||||
context = factoryContext,
|
||||
params = SendDestinationComponentParams.DestinationParams(
|
||||
state = model.uiState.value.destinationUM,
|
||||
route = route,
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
onLoadFee = model::loadFee,
|
||||
title = resourceReference(R.string.nft_send),
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
analyticsSendSource = analyticsSendSource,
|
||||
account = model.account,
|
||||
isAccountsMode = model.isAccountsMode,
|
||||
onSendTransaction = { innerRouter.replaceAll(CommonSendRoute.ConfirmSuccess) },
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = model.cryptoCurrency,
|
||||
callback = model,
|
||||
),
|
||||
)
|
||||
|
||||
private fun getSuccessComponent(factoryContext: AppComponentContext): ComposableContentComponent {
|
||||
private fun getConfirmComponent(factoryContext: AppComponentContext): ComposableModularContentComponent {
|
||||
return nftSendConfirmComponentFactory.create(
|
||||
appComponentContext = factoryContext,
|
||||
params = NFTSendConfirmComponent.Params(
|
||||
state = model.uiState.value,
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
userWallet = model.userWallet,
|
||||
nftAsset = params.nftAsset,
|
||||
nftCollectionName = params.nftCollectionName,
|
||||
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
|
||||
feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus,
|
||||
appCurrency = model.appCurrency,
|
||||
callback = model,
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
onLoadFee = model::loadFee,
|
||||
analyticsSendSource = analyticsSendSource,
|
||||
account = model.account,
|
||||
isAccountsMode = model.isAccountsMode,
|
||||
onSendTransaction = { innerRouter.replaceAll(CommonSendRoute.ConfirmSuccess) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSuccessComponent(factoryContext: AppComponentContext): ComposableModularContentComponent {
|
||||
val txUrl = (model.uiState.value.confirmUM as? ConfirmUM.Success)?.txUrl
|
||||
|
||||
if (txUrl == null) {
|
||||
model.showAlertError()
|
||||
return getStubComponent()
|
||||
return ComposableModularContentComponent.EMPTY
|
||||
}
|
||||
|
||||
return nftSendSuccessComponentFactory.create(
|
||||
|
|
@ -188,7 +189,6 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
|
|||
nftAsset = params.nftAsset,
|
||||
nftCollectionName = params.nftCollectionName,
|
||||
callback = model,
|
||||
currentRoute = model.currentRouteFlow.filterIsInstance<CommonSendRoute.ConfirmSuccess>(),
|
||||
txUrl = txUrl,
|
||||
account = model.account,
|
||||
isAccountsMode = model.isAccountsMode,
|
||||
|
|
@ -196,8 +196,6 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getStubComponent() = ComposableContentComponent { }
|
||||
|
||||
private fun onChildBack() {
|
||||
val isEmptyRoute = childStack.value.active.configuration == CommonSendRoute.Empty
|
||||
val isEmptyStack = childStack.value.backStack.isEmpty()
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import com.tangem.core.analytics.models.Basic
|
|||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents.Companion.NFT_SEND_CATEGORY
|
||||
import com.tangem.features.send.api.entity.FeeNonce
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationTextFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.sendnft.ui.state.NFTSendUM
|
||||
|
|
|
|||
|
|
@ -1,17 +1,28 @@
|
|||
package com.tangem.features.send.sendnft.confirm
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import arrow.core.Either
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.footers.SendingText
|
||||
import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton
|
||||
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.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
|
|
@ -19,21 +30,20 @@ import com.tangem.domain.models.wallet.UserWallet
|
|||
import com.tangem.domain.nft.models.NFTAsset
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.features.nft.component.NFTDetailsBlockComponent
|
||||
import com.tangem.features.send.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.api.SendNotificationsComponent
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.api.params.FeeSelectorParams
|
||||
import com.tangem.features.send.api.params.FeeSelectorParams.FeeStateConfiguration
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams.FeeStateConfiguration
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.sendnft.confirm.model.NFTSendConfirmModel
|
||||
import com.tangem.features.send.sendnft.confirm.ui.NFTSendConfirmContent
|
||||
import com.tangem.features.send.sendnft.ui.state.NFTSendUM
|
||||
import com.tangem.features.send.subcomponents.destination.DefaultSendDestinationBlockComponent
|
||||
import com.tangem.features.send.subcomponents.notifications.DefaultSendNotificationsComponent
|
||||
import com.tangem.features.send.impl.R
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -45,7 +55,7 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
|
|||
@Assisted params: Params,
|
||||
nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory,
|
||||
feeSelectorComponentFactory: FeeSelectorBlockComponent.Factory,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: NFTSendConfirmModel = getOrCreateModel(params = params)
|
||||
|
||||
|
|
@ -129,6 +139,20 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
|
|||
model.updateState(state)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = stringResourceSafe(R.string.nft_send),
|
||||
onBackClick = {
|
||||
model.onBackClick()
|
||||
router.pop()
|
||||
},
|
||||
backIconRes = R.drawable.ic_back_24,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
|
@ -144,6 +168,29 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
Column {
|
||||
val sendingFooter = (state.confirmUM as? ConfirmUM.Content)?.sendingFooter
|
||||
AnimatedVisibility(
|
||||
visible = sendingFooter != null,
|
||||
enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(),
|
||||
exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(),
|
||||
) {
|
||||
SendingText(footerText = sendingFooter ?: TextReference.EMPTY)
|
||||
}
|
||||
NavigationPrimaryButton(
|
||||
primaryButton = model.primaryButtonUM(),
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val state: NFTSendUM,
|
||||
val analyticsCategoryName: String,
|
||||
|
|
@ -157,7 +204,6 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
|
|||
val account: Account.CryptoPortfolio?,
|
||||
val isAccountsMode: Boolean,
|
||||
val callback: ModelCallback,
|
||||
val currentRoute: Flow<CommonSendRoute.Confirm>,
|
||||
val isBalanceHidingFlow: StateFlow<Boolean>,
|
||||
val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
|
||||
val onSendTransaction: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import arrow.core.getOrElse
|
|||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
|
@ -32,28 +31,28 @@ import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase
|
|||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.features.nft.entity.NFTSendSuccessTrigger
|
||||
import com.tangem.features.send.api.SendNotificationsComponent
|
||||
import com.tangem.features.send.api.SendNotificationsComponent.Params.NotificationData
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents.SendScreenSource
|
||||
import com.tangem.features.send.api.callbacks.FeeSelectorModelCallback
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorCheckReloadListener
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorCheckReloadTrigger
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.callbacks.FeeSelectorModelCallback
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent.Params.NotificationData
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateListener
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateTrigger
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.common.SendBalanceUpdater
|
||||
import com.tangem.features.send.common.SendConfirmAlertFactory
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.sendnft.analytics.NFTSendAnalyticHelper
|
||||
import com.tangem.features.send.sendnft.confirm.NFTSendConfirmComponent
|
||||
import com.tangem.features.send.sendnft.confirm.model.transformers.NFTSendConfirmInitialStateTransformer
|
||||
import com.tangem.features.send.sendnft.confirm.model.transformers.NFTSendConfirmSendingStateTransformer
|
||||
import com.tangem.features.send.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformerV2
|
||||
import com.tangem.features.send.sendnft.ui.state.NFTSendUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.stripZeroPlainString
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
|
@ -62,7 +61,7 @@ import kotlinx.coroutines.flow.*
|
|||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM as FeeSelectorUMRedesigned
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
@ModelScoped
|
||||
|
|
@ -122,7 +121,6 @@ internal class NFTSendConfirmModel @Inject constructor(
|
|||
private var sendIdleTimer: Long = 0L
|
||||
|
||||
init {
|
||||
configConfirmNavigation()
|
||||
subscribeOnNotificationsUpdateTrigger()
|
||||
subscribeOnCheckFeeResultUpdates()
|
||||
subscribeOnTapHelpUpdates()
|
||||
|
|
@ -371,54 +369,18 @@ internal class NFTSendConfirmModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
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(
|
||||
source = CommonSendRoute.Confirm.javaClass.simpleName,
|
||||
title = resourceReference(R.string.nft_send),
|
||||
subtitle = null,
|
||||
backIconRes = when (confirmUM) {
|
||||
is ConfirmUM.Success -> R.drawable.ic_close_24
|
||||
else -> R.drawable.ic_back_24
|
||||
},
|
||||
backIconClick = {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = analyticsCategoryName,
|
||||
source = SendScreenSource.Confirm,
|
||||
isFromSummary = true,
|
||||
isValid = confirmUM.isPrimaryButtonEnabled,
|
||||
),
|
||||
)
|
||||
router.pop()
|
||||
},
|
||||
primaryButton = primaryButtonUM(),
|
||||
prevButton = null,
|
||||
secondaryPairButtonsUM = (
|
||||
NavigationButton(
|
||||
textReference = resourceReference(R.string.common_explore),
|
||||
iconRes = R.drawable.ic_web_24,
|
||||
onClick = ::onExploreClick,
|
||||
) to NavigationButton(
|
||||
textReference = resourceReference(R.string.common_share),
|
||||
iconRes = R.drawable.ic_share_24,
|
||||
onClick = ::onShareClick,
|
||||
)
|
||||
).takeUnless { (confirmUM as? ConfirmUM.Success)?.txUrl.isNullOrBlank() },
|
||||
),
|
||||
),
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
fun onBackClick() {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = analyticsCategoryName,
|
||||
source = SendScreenSource.Confirm,
|
||||
isFromSummary = true,
|
||||
isValid = uiState.value.confirmUM.isPrimaryButtonEnabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun primaryButtonUM(): NavigationButton {
|
||||
fun primaryButtonUM(): NavigationButton {
|
||||
val confirmUM = uiState.value.confirmUM
|
||||
val isContent = confirmUM is ConfirmUM.Content
|
||||
val isReadyToSend = isContent && !confirmUM.isSending
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import com.tangem.core.ui.extensions.wrappedList
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils
|
||||
import com.tangem.features.send.api.utils.formatFooterFiatFee
|
||||
import com.tangem.features.send.api.utils.getTronTokenFeeSendingText
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import androidx.compose.ui.unit.dp
|
|||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.nft.component.NFTDetailsBlockComponent
|
||||
import com.tangem.features.send.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.common.ui.tapHelp
|
||||
import com.tangem.features.send.sendnft.ui.state.NFTSendUM
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ import arrow.core.Either
|
|||
import arrow.core.getOrElse
|
||||
import arrow.core.left
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
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.Route
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
|
|
@ -36,9 +36,9 @@ import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
|||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.nft.entity.NFTSendSuccessTrigger
|
||||
import com.tangem.features.send.api.NFTSendComponent
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponent
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.common.SendConfirmAlertFactory
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
|
|
@ -82,8 +82,6 @@ internal class NFTSendModel @Inject constructor(
|
|||
|
||||
val initialRoute = CommonSendRoute.Empty
|
||||
|
||||
val currentRouteFlow = MutableStateFlow<CommonSendRoute>(initialRoute)
|
||||
|
||||
private val userWalletId = params.userWalletId
|
||||
private val nftAsset = params.nftAsset
|
||||
|
||||
|
|
@ -107,10 +105,6 @@ internal class NFTSendModel @Inject constructor(
|
|||
initAppCurrency()
|
||||
}
|
||||
|
||||
override fun onNavigationResult(navigationUM: NavigationUM) {
|
||||
uiState.update { it.copy(navigationUM = navigationUM) }
|
||||
}
|
||||
|
||||
override fun onResult(nftSendUM: NFTSendUM) {
|
||||
uiState.value = nftSendUM
|
||||
}
|
||||
|
|
@ -119,8 +113,8 @@ internal class NFTSendModel @Inject constructor(
|
|||
uiState.update { it.copy(destinationUM = destinationUM) }
|
||||
}
|
||||
|
||||
override fun onBackClick() {
|
||||
if (currentRouteFlow.value == CommonSendRoute.ConfirmSuccess) {
|
||||
override fun onBackClick(currentRoute: Route) {
|
||||
if (currentRoute == CommonSendRoute.ConfirmSuccess) {
|
||||
modelScope.launch {
|
||||
nftSendSuccessTrigger.triggerSuccessNFTSend()
|
||||
}
|
||||
|
|
@ -128,14 +122,14 @@ internal class NFTSendModel @Inject constructor(
|
|||
router.pop()
|
||||
}
|
||||
|
||||
override fun onNextClick() {
|
||||
if (currentRouteFlow.value.isEditMode) {
|
||||
onBackClick()
|
||||
override fun onNextClick(currentRoute: Route) {
|
||||
if ((currentRoute as? CommonSendRoute)?.isEditMode == true) {
|
||||
onBackClick(currentRoute)
|
||||
} else {
|
||||
when (currentRouteFlow.value) {
|
||||
when (currentRoute) {
|
||||
is CommonSendRoute.Destination -> router.push(CommonSendRoute.Confirm)
|
||||
CommonSendRoute.Confirm -> router.replaceAll(CommonSendRoute.ConfirmSuccess)
|
||||
else -> onBackClick()
|
||||
else -> onBackClick(currentRoute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -241,6 +235,5 @@ internal class NFTSendModel @Inject constructor(
|
|||
destinationUM = DestinationUM.Empty(),
|
||||
feeSelectorUM = FeeSelectorUM.Loading,
|
||||
confirmUM = ConfirmUM.Empty,
|
||||
navigationUM = NavigationUM.Empty,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,15 +1,27 @@
|
|||
package com.tangem.features.send.sendnft.success
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.common.ui.navigationButtons.DoneButtons
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationModelCallback
|
||||
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.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -19,24 +31,22 @@ import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
|||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationBlockComponent
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.sendnft.success.model.NFTSendSuccessModel
|
||||
import com.tangem.features.send.sendnft.success.ui.NFTSendSuccessContent
|
||||
import com.tangem.features.send.sendnft.ui.state.NFTSendUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
internal class NFTSendSuccessComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: Params,
|
||||
@Assisted private val params: Params,
|
||||
nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory,
|
||||
sendDestinationBlockComponentFactory: SendDestinationBlockComponent.Factory,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: NFTSendSuccessModel = getOrCreateModel(params = params)
|
||||
|
||||
|
|
@ -63,11 +73,25 @@ internal class NFTSendSuccessComponent @AssistedInject constructor(
|
|||
cryptoCurrency = params.cryptoCurrencyStatus.currency,
|
||||
blockClickEnableFlow = MutableStateFlow(false),
|
||||
predefinedValues = PredefinedValues.Empty,
|
||||
isAddContactAvailable = true,
|
||||
),
|
||||
onResult = {},
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
AppBarWithBackButtonAndIcon(
|
||||
onBackClick = {
|
||||
model.onBackClick()
|
||||
router.pop()
|
||||
},
|
||||
backIconRes = R.drawable.ic_close_24,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
|
@ -79,11 +103,40 @@ internal class NFTSendSuccessComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
) {
|
||||
DoneButtons(
|
||||
(NavigationButton(
|
||||
textReference = resourceReference(R.string.common_explore),
|
||||
iconRes = R.drawable.ic_web_24,
|
||||
onClick = model::onExploreClick,
|
||||
) to NavigationButton(
|
||||
textReference = resourceReference(R.string.common_share),
|
||||
iconRes = R.drawable.ic_share_24,
|
||||
onClick = model::onShareClick,
|
||||
)).takeIf { params.txUrl.isNotEmpty() },
|
||||
)
|
||||
PrimaryButton(
|
||||
text = stringResourceSafe(R.string.common_close),
|
||||
onClick = router::pop,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val nftSendUMFlow: StateFlow<NFTSendUM>,
|
||||
val analyticsCategoryName: String,
|
||||
val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
|
||||
val currentRoute: Flow<CommonSendRoute>,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val userWallet: UserWallet,
|
||||
val nftAsset: NFTAsset,
|
||||
|
|
|
|||
|
|
@ -1,27 +1,17 @@
|
|||
package com.tangem.features.send.sendnft.success.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
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.navigation.share.ShareManager
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents.SendScreenSource
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
import com.tangem.features.send.sendnft.success.NFTSendSuccessComponent
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
|
|
@ -37,71 +27,23 @@ internal class NFTSendSuccessModel @Inject constructor(
|
|||
|
||||
val uiState = params.nftSendUMFlow
|
||||
|
||||
init {
|
||||
configConfirmSuccessNavigation()
|
||||
fun onBackClick() {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
source = SendScreenSource.Confirm,
|
||||
isFromSummary = true,
|
||||
isValid = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun configConfirmSuccessNavigation() {
|
||||
combine(
|
||||
flow = uiState,
|
||||
flow2 = params.currentRoute,
|
||||
transform = { state, route -> state to route },
|
||||
).filter { (state, route) ->
|
||||
// Emit the success navigation exactly once. Building NavigationUM.Content here creates fresh
|
||||
// lambdas every time, so the SendUM written back via callback.onResult is never equal to the
|
||||
// previous one — without this guard the combine re-triggers itself endlessly and the success
|
||||
// screen recomposes forever (never reaching Compose idle). See [REDACTED_TASK_KEY].
|
||||
route is CommonSendRoute.ConfirmSuccess &&
|
||||
(state.navigationUM as? NavigationUM.Content)?.source !=
|
||||
CommonSendRoute.ConfirmSuccess.javaClass.simpleName
|
||||
}.onEach { (state, _) ->
|
||||
params.callback.onResult(
|
||||
state.copy(
|
||||
navigationUM = NavigationUM.Content(
|
||||
source = CommonSendRoute.ConfirmSuccess.javaClass.simpleName,
|
||||
title = stringReference(""),
|
||||
subtitle = null,
|
||||
backIconRes = R.drawable.ic_close_24,
|
||||
backIconClick = {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
source = SendScreenSource.Confirm,
|
||||
isFromSummary = true,
|
||||
isValid = true,
|
||||
),
|
||||
)
|
||||
params.callback.onBackClick()
|
||||
},
|
||||
primaryButton = NavigationButton(
|
||||
textReference = resourceReference(R.string.common_close),
|
||||
iconRes = null,
|
||||
isEnabled = true,
|
||||
isHapticClick = false,
|
||||
onClick = params.callback::onBackClick,
|
||||
),
|
||||
prevButton = null,
|
||||
secondaryPairButtonsUM = NavigationButton(
|
||||
textReference = resourceReference(R.string.common_explore),
|
||||
iconRes = R.drawable.ic_web_24,
|
||||
onClick = ::onExploreClick,
|
||||
) to NavigationButton(
|
||||
textReference = resourceReference(R.string.common_share),
|
||||
iconRes = R.drawable.ic_share_24,
|
||||
onClick = ::onShareClick,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onExploreClick() {
|
||||
fun onExploreClick() {
|
||||
analyticsEventHandler.send(CommonSendAnalyticEvents.ExploreButtonClicked(params.analyticsCategoryName))
|
||||
urlOpener.openUrl(params.txUrl)
|
||||
}
|
||||
|
||||
private fun onShareClick() {
|
||||
fun onShareClick() {
|
||||
analyticsEventHandler.send(CommonSendAnalyticEvents.ShareButtonClicked(params.analyticsCategoryName))
|
||||
shareManager.shareText(params.txUrl)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import androidx.compose.runtime.*
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2
|
||||
import com.tangem.core.ui.components.Fade
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
|
||||
|
|
@ -23,8 +22,8 @@ import com.tangem.features.nft.component.NFTDetailsBlockComponent
|
|||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationBlockComponent
|
||||
import com.tangem.features.send.common.ui.FeeBlockSuccess
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.sendnft.ui.state.NFTSendUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.sendnft.ui.state.NFTSendUM
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
|
|
@ -52,30 +51,20 @@ internal fun NFTSendSuccessContent(
|
|||
label = "Animate success content",
|
||||
modifier = modifier,
|
||||
) {
|
||||
Column {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
SuccessContent(
|
||||
nftSendUM = nftSendUM,
|
||||
nftDetailsBlockComponent = nftDetailsBlockComponent,
|
||||
destinationBlockComponent = destinationBlockComponent,
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
)
|
||||
Fade(
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
)
|
||||
}
|
||||
NavigationButtonsBlockV2(
|
||||
navigationUM = nftSendUM.navigationUM,
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
SuccessContent(
|
||||
nftSendUM = nftSendUM,
|
||||
nftDetailsBlockComponent = nftDetailsBlockComponent,
|
||||
destinationBlockComponent = destinationBlockComponent,
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
)
|
||||
Fade(
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
package com.tangem.features.send.sendnft.ui.state
|
||||
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
|
||||
internal data class NFTSendUM(
|
||||
val destinationUM: DestinationUM,
|
||||
val feeSelectorUM: FeeSelectorUM,
|
||||
val confirmUM: ConfirmUM,
|
||||
val navigationUM: NavigationUM,
|
||||
)
|
||||
|
|
@ -8,18 +8,22 @@ import com.tangem.common.ui.amountScreen.models.AmountState
|
|||
import com.tangem.common.ui.amountScreen.ui.AmountBlockV2
|
||||
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.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountBlockComponent
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountComponentParams
|
||||
import com.tangem.features.send.subcomponents.amount.model.SendAmountModel
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
internal class SendAmountBlockComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: SendAmountComponentParams.AmountBlockParams,
|
||||
val onResult: (AmountState) -> Unit,
|
||||
val onClick: () -> Unit,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
internal class DefaultSendAmountBlockComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: SendAmountComponentParams.AmountBlockParams,
|
||||
@Assisted val onResult: (AmountState) -> Unit,
|
||||
@Assisted val onClick: () -> Unit,
|
||||
) : SendAmountBlockComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SendAmountModel = getOrCreateModel(params = params)
|
||||
|
||||
|
|
@ -29,7 +33,7 @@ internal class SendAmountBlockComponent(
|
|||
}.launchIn(componentScope)
|
||||
}
|
||||
|
||||
fun updateState(amountUM: AmountState) = model.updateState(amountUM)
|
||||
override fun updateState(amountUM: AmountState) = model.updateState(amountUM)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
|
|
@ -44,4 +48,14 @@ internal class SendAmountBlockComponent(
|
|||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SendAmountBlockComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: SendAmountComponentParams.AmountBlockParams,
|
||||
onClick: () -> Unit,
|
||||
onResult: (AmountState) -> Unit,
|
||||
): DefaultSendAmountBlockComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.tangem.features.send.subcomponents.amount
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountComponent
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountComponentParams
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.subcomponents.amount.model.SendAmountModel
|
||||
import com.tangem.features.send.subcomponents.amount.ui.SendAmountContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultSendAmountComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: SendAmountComponentParams.AmountParams,
|
||||
) : SendAmountComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SendAmountModel = getOrCreateModel(params = params)
|
||||
|
||||
override fun updateState(amountUM: AmountState) = model.updateState(amountUM)
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = stringResourceSafe(R.string.send_amount_label),
|
||||
onBackClick = {
|
||||
params.callback.onBackClick(params.route)
|
||||
},
|
||||
backIconRes = if (params.route.isEditMode) {
|
||||
R.drawable.ic_back_24
|
||||
} else {
|
||||
R.drawable.ic_close_24
|
||||
},
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
val isSendWithSwapAvailable by model.isSendWithSwapAvailable.collectAsStateWithLifecycle()
|
||||
|
||||
SendAmountContent(
|
||||
amountState = state,
|
||||
clickIntents = model,
|
||||
isSendWithSwapAvailable = isSendWithSwapAvailable,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
PrimaryButton(
|
||||
text = if (params.route.isEditMode) {
|
||||
stringResourceSafe(R.string.common_continue)
|
||||
} else {
|
||||
stringResourceSafe(R.string.common_next)
|
||||
},
|
||||
enabled = state.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
model.onAmountNext()
|
||||
params.callback.onNextClick(params.route)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SendAmountComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: SendAmountComponentParams.AmountParams,
|
||||
): DefaultSendAmountComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +1,15 @@
|
|||
package com.tangem.features.send.subcomponents.amount
|
||||
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountReduceListener
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountReduceTrigger
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountUpdateListener
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountUpdateTrigger
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Trigger for reducing amount from another component
|
||||
*/
|
||||
interface SendAmountReduceTrigger {
|
||||
suspend fun triggerReduceBy(reduceBy: ReduceByData)
|
||||
suspend fun triggerReduceTo(reduceTo: BigDecimal)
|
||||
suspend fun triggerIgnoreReduce()
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger for reducing amount from another component
|
||||
*/
|
||||
interface SendAmountReduceListener {
|
||||
val reduceToTriggerFlow: Flow<BigDecimal>
|
||||
val reduceByTriggerFlow: Flow<ReduceByData>
|
||||
val ignoreReduceTriggerFlow: Flow<Unit>
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger amount change from another component.
|
||||
* Different from another triggers because it takes raw string instead of BigDecimal
|
||||
*/
|
||||
interface SendAmountUpdateTrigger {
|
||||
suspend fun triggerUpdateAmount(amountValue: String, isEnterInFiatSelected: Boolean?)
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger amount change from another component.
|
||||
* Different from another triggers because it takes raw string instead of BigDecimal
|
||||
*/
|
||||
interface SendAmountUpdateListener {
|
||||
val updateAmountTriggerFlow: Flow<Pair<String, Boolean?>>
|
||||
}
|
||||
|
||||
@Singleton
|
||||
internal class DefaultSendAmountReduceTrigger @Inject constructor() :
|
||||
SendAmountReduceTrigger,
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
package com.tangem.features.send.subcomponents.amount
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.navigationButtons.NavigationModelCallback
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.wallets.models.GetUserWalletError
|
||||
import com.tangem.features.send.subcomponents.amount.model.SendAmountModel
|
||||
import com.tangem.features.send.subcomponents.amount.ui.SendAmountContent
|
||||
|
||||
internal class SendAmountComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: SendAmountComponentParams.AmountParams,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SendAmountModel = getOrCreateModel(params = params)
|
||||
|
||||
fun updateState(amountUM: AmountState) = model.updateState(amountUM)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
val isSendWithSwapAvailable by model.isSendWithSwapAvailable.collectAsStateWithLifecycle()
|
||||
|
||||
SendAmountContent(
|
||||
amountState = state,
|
||||
clickIntents = model,
|
||||
isSendWithSwapAvailable = isSendWithSwapAvailable,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelCallback : NavigationModelCallback {
|
||||
fun onAmountResult(amountUM: AmountState, isResetPredefined: Boolean)
|
||||
fun onConvertToAnotherToken(lastAmount: String, isEnterInFiatSelected: Boolean)
|
||||
fun resetSendNavigation()
|
||||
fun onError(error: GetUserWalletError)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
package com.tangem.features.send.subcomponents.amount
|
||||
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
internal sealed class SendAmountComponentParams {
|
||||
|
||||
abstract val state: AmountState
|
||||
abstract val analyticsCategoryName: String
|
||||
abstract val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource
|
||||
abstract val userWalletId: UserWalletId
|
||||
abstract val appCurrency: AppCurrency
|
||||
abstract val predefinedValues: PredefinedValues
|
||||
abstract val cryptoCurrency: CryptoCurrency
|
||||
abstract val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
|
||||
abstract val isBalanceHidingFlow: StateFlow<Boolean>
|
||||
abstract val accountFlow: StateFlow<Account?>
|
||||
abstract val isAccountModeFlow: StateFlow<Boolean>
|
||||
|
||||
data class AmountParams(
|
||||
override val state: AmountState,
|
||||
override val analyticsCategoryName: String,
|
||||
override val userWalletId: UserWalletId,
|
||||
override val appCurrency: AppCurrency,
|
||||
override val predefinedValues: PredefinedValues,
|
||||
override val cryptoCurrency: CryptoCurrency,
|
||||
override val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
|
||||
override val isBalanceHidingFlow: StateFlow<Boolean>,
|
||||
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
|
||||
override val accountFlow: StateFlow<Account?>,
|
||||
override val isAccountModeFlow: StateFlow<Boolean>,
|
||||
val callback: SendAmountComponent.ModelCallback,
|
||||
val currentRoute: StateFlow<CommonSendRoute>,
|
||||
) : SendAmountComponentParams()
|
||||
|
||||
data class AmountBlockParams(
|
||||
override val state: AmountState,
|
||||
override val analyticsCategoryName: String,
|
||||
override val appCurrency: AppCurrency,
|
||||
override val userWalletId: UserWalletId,
|
||||
override val predefinedValues: PredefinedValues,
|
||||
override val cryptoCurrency: CryptoCurrency,
|
||||
override val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
|
||||
override val isBalanceHidingFlow: StateFlow<Boolean>,
|
||||
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
|
||||
override val accountFlow: StateFlow<Account?>,
|
||||
override val isAccountModeFlow: StateFlow<Boolean>,
|
||||
val userWallet: UserWallet,
|
||||
val blockClickEnableFlow: StateFlow<Boolean>,
|
||||
) : SendAmountComponentParams()
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
package com.tangem.features.send.subcomponents.amount.di
|
||||
|
||||
import com.tangem.features.send.api.subcomponents.amount.*
|
||||
import com.tangem.features.send.subcomponents.amount.DefaultSendAmountBlockComponent
|
||||
import com.tangem.features.send.subcomponents.amount.DefaultSendAmountComponent
|
||||
import com.tangem.features.send.subcomponents.amount.DefaultSendAmountReduceTrigger
|
||||
import com.tangem.features.send.subcomponents.amount.SendAmountReduceListener
|
||||
import com.tangem.features.send.subcomponents.amount.SendAmountReduceTrigger
|
||||
import com.tangem.features.send.subcomponents.amount.SendAmountUpdateListener
|
||||
import com.tangem.features.send.subcomponents.amount.SendAmountUpdateTrigger
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -15,6 +14,16 @@ import javax.inject.Singleton
|
|||
@Module
|
||||
internal interface SendAmountModule {
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun provideSendAmountComponentFactory(impl: DefaultSendAmountComponent.Factory): SendAmountComponent.Factory
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun provideSendAmountBlockComponentFactory(
|
||||
impl: DefaultSendAmountBlockComponent.Factory,
|
||||
): SendAmountBlockComponent.Factory
|
||||
|
||||
@Singleton
|
||||
@Binds
|
||||
fun provideSendAmountReduceTrigger(impl: DefaultSendAmountReduceTrigger): SendAmountReduceTrigger
|
||||
|
|
|
|||
|
|
@ -9,13 +9,11 @@ import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmoun
|
|||
import com.tangem.common.ui.amountScreen.models.AmountParameters
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
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.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.WrappedList
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
|
|
@ -29,19 +27,19 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
|||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.api.entity.isFromMainScreenQr
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountComponentParams
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountReduceListener
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountUpdateListener
|
||||
import com.tangem.features.send.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents
|
||||
import com.tangem.features.send.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents.SelectedCurrencyType
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.subcomponents.amount.SendAmountComponentParams
|
||||
import com.tangem.features.send.subcomponents.amount.SendAmountReduceListener
|
||||
import com.tangem.features.send.subcomponents.amount.SendAmountUpdateListener
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
|
|
@ -82,7 +80,6 @@ internal class SendAmountModel @Inject constructor(
|
|||
private var maxAmountBoundary: EnterAmountBoundary by Delegates.notNull()
|
||||
|
||||
init {
|
||||
configAmountNavigation()
|
||||
initAppCurrency()
|
||||
subscribeOnCryptoCurrencyStatusFlow()
|
||||
subscribeOnAmountReduceByTriggerUpdates()
|
||||
|
|
@ -102,6 +99,7 @@ internal class SendAmountModel @Inject constructor(
|
|||
},
|
||||
ifRight = { wallet ->
|
||||
userWallet = wallet
|
||||
setSendWithSwapAvailability()
|
||||
},
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
|
|
@ -271,13 +269,15 @@ internal class SendAmountModel @Inject constructor(
|
|||
|
||||
override fun onConvertToAnotherToken() {
|
||||
val amountParams = params as? SendAmountComponentParams.AmountParams ?: return
|
||||
if (amountParams.currentRoute.value.isEditMode) {
|
||||
sendAmountAlertFactory.showResetSendingAlert {
|
||||
params.callback.resetSendNavigation()
|
||||
modelScope.launch {
|
||||
if (amountParams.route.isEditMode) {
|
||||
sendAmountAlertFactory.showResetSendingAlert {
|
||||
params.callback.resetSendNavigation()
|
||||
confirmConvertToToken()
|
||||
}
|
||||
} else {
|
||||
confirmConvertToToken()
|
||||
}
|
||||
} else {
|
||||
confirmConvertToToken()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -357,42 +357,6 @@ internal class SendAmountModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun configAmountNavigation() {
|
||||
val params = params as? SendAmountComponentParams.AmountParams ?: return
|
||||
combine(
|
||||
flow = uiState,
|
||||
flow2 = params.currentRoute.filterIsInstance<CommonSendRoute.Amount>(),
|
||||
transform = { state, route -> state to route },
|
||||
).onEach { (state, route) ->
|
||||
setSendWithSwapAvailability()
|
||||
params.callback.onNavigationResult(
|
||||
NavigationUM.Content(
|
||||
source = CommonSendRoute.Amount::class.java.simpleName,
|
||||
title = resourceReference(R.string.send_amount_label),
|
||||
subtitle = null,
|
||||
backIconRes = if (route.isEditMode) {
|
||||
R.drawable.ic_back_24
|
||||
} else {
|
||||
R.drawable.ic_close_24
|
||||
},
|
||||
backIconClick = params.callback::onBackClick,
|
||||
primaryButton = NavigationButton(
|
||||
textReference = if (route.isEditMode) {
|
||||
resourceReference(R.string.common_continue)
|
||||
} else {
|
||||
resourceReference(R.string.common_next)
|
||||
},
|
||||
isEnabled = state.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
onAmountNext()
|
||||
params.callback.onNextClick()
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun setSendWithSwapAvailability() {
|
||||
// Allowed only in multicurrency wallets
|
||||
val isMultiCurrency = userWallet?.isMultiCurrency == true
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ private fun SendConvertTokenButton(onConvertToAnother: () -> Unit) {
|
|||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth()
|
||||
.testTag(SendScreenTestTags.SWAP_TO_ANOTHER_TOKEN_BUTTON)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = null,
|
||||
|
|
@ -67,8 +68,7 @@ private fun SendConvertTokenButton(onConvertToAnother: () -> Unit) {
|
|||
Row(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.align(Alignment.Center)
|
||||
.testTag(SendScreenTestTags.SWAP_TO_ANOTHER_TOKEN_BUTTON),
|
||||
.align(Alignment.Center),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -39,12 +39,15 @@ internal class DefaultSendDestinationBlockComponent @AssistedInject constructor(
|
|||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
val isClickEnabled by params.blockClickEnableFlow.collectAsStateWithLifecycle()
|
||||
val isAddContactVisible by model.showAddContact.collectAsStateWithLifecycle()
|
||||
|
||||
DestinationBlock(
|
||||
destinationUM = state,
|
||||
isClickDisabled = !isClickEnabled,
|
||||
isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink,
|
||||
onClick = onClick,
|
||||
showAddContact = isAddContactVisible,
|
||||
onAddContactClick = model::onAddContactClick,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,33 @@
|
|||
package com.tangem.features.send.subcomponents.destination
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.addressbook.AddressBookContactsBlockComponent
|
||||
import com.tangem.features.addressbook.AddressBookFeatureToggles
|
||||
import com.tangem.features.addressbook.AddressSelectorComponent
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponent
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponentParams
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.subcomponents.destination.model.SendDestinationModel
|
||||
import com.tangem.features.send.subcomponents.destination.ui.SendDestinationContent
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -18,18 +37,101 @@ import dagger.assisted.AssistedInject
|
|||
internal class DefaultSendDestinationComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: SendDestinationComponentParams.DestinationParams,
|
||||
addressBookFeatureToggles: AddressBookFeatureToggles,
|
||||
contactsBlockFactory: AddressBookContactsBlockComponent.Factory,
|
||||
addressSelectorFactory: AddressSelectorComponent.Factory,
|
||||
) : SendDestinationComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SendDestinationModel = getOrCreateModel(params = params)
|
||||
|
||||
private val contactsBlock: AddressBookContactsBlockComponent? by lazy {
|
||||
if (addressBookFeatureToggles.isAddressBookEnabled) {
|
||||
contactsBlockFactory.create(
|
||||
context = child("send_contacts_block"),
|
||||
params = AddressBookContactsBlockComponent.Params(
|
||||
network = params.cryptoCurrency.network,
|
||||
queryFlow = model.addressQuery,
|
||||
onContactClick = model::onContactClick,
|
||||
onSeeAllClick = model::onSeeAllContactsClick,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private val addressSelectorSlot = childSlot(
|
||||
source = model.addressSelectorNavigation,
|
||||
serializer = null,
|
||||
key = "send_address_selector_slot",
|
||||
handleBackButton = true,
|
||||
childFactory = { contact, componentContext ->
|
||||
addressSelectorFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = AddressSelectorComponent.Params(
|
||||
contact = contact,
|
||||
onAddressSelected = model::applySelectedContact,
|
||||
onDismiss = { model.addressSelectorNavigation.dismiss() },
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
override fun updateState(destinationUM: DestinationUM) = model.updateState(destinationUM)
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
BackHandler(onBack = model::onBackClick)
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = params.title.resolveReference(),
|
||||
onBackClick = model::onBackClick,
|
||||
backIconRes = if (params.route.isEditMode) {
|
||||
R.drawable.ic_back_24
|
||||
} else {
|
||||
R.drawable.ic_close_24
|
||||
},
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
val isBalanceHidden by params.isBalanceHidingFlow.collectAsStateWithLifecycle()
|
||||
val selector by addressSelectorSlot.subscribeAsState()
|
||||
|
||||
SendDestinationContent(state = state, clickIntents = model, isBalanceHidden = isBalanceHidden)
|
||||
SendDestinationContent(
|
||||
state = state,
|
||||
clickIntents = model,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
contactsBlock = contactsBlock,
|
||||
)
|
||||
selector.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
PrimaryButton(
|
||||
text = if (params.route.isEditMode) {
|
||||
stringResourceSafe(R.string.common_continue)
|
||||
} else {
|
||||
stringResourceSafe(R.string.common_next)
|
||||
},
|
||||
enabled = state.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
model.saveResult()
|
||||
params.callback.onNextClick(params.route)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ internal enum class EnterAddressSource {
|
|||
RecentAddress,
|
||||
InputField,
|
||||
MyWallets,
|
||||
Contact,
|
||||
;
|
||||
|
||||
val isPasted: Boolean
|
||||
get() = this != InputField
|
||||
|
||||
val isAutoNext: Boolean
|
||||
get() = this == RecentAddress || this == MyWallets
|
||||
get() = this == RecentAddress || this == MyWallets || this == Contact
|
||||
}
|
||||
|
|
@ -3,18 +3,22 @@ package com.tangem.features.send.subcomponents.destination.model
|
|||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.left
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.common.routing.entity.AddressBookOpenMode
|
||||
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.extensions.resourceReference
|
||||
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
|
||||
import com.tangem.domain.account.status.usecase.GetBackupProblematicWalletForAddressUseCase
|
||||
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.addressbook.interactor.GetVerifiedContactsInteractor
|
||||
import com.tangem.domain.addressbook.model.Contact
|
||||
import com.tangem.domain.addressbook.usecase.SyncAddressBooksUseCase
|
||||
import com.tangem.domain.feedback.SendBackupProblemEmailUseCase
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
|
|
@ -34,25 +38,18 @@ import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
|
|||
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.addressbook.*
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents.SendScreenSource
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponentParams
|
||||
import com.tangem.features.send.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.common.CommonSendRoute
|
||||
import com.tangem.features.send.subcomponents.destination.SendDestinationAlertFactory
|
||||
import com.tangem.features.send.subcomponents.destination.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.subcomponents.destination.analytics.SendDestinationAnalyticEvents
|
||||
import com.tangem.features.send.subcomponents.destination.model.transformers.SendDestinationAddressTransformer
|
||||
import com.tangem.features.send.subcomponents.destination.model.transformers.SendDestinationInitialStateTransformer
|
||||
import com.tangem.features.send.subcomponents.destination.model.transformers.SendDestinationMemoTransformer
|
||||
import com.tangem.features.send.subcomponents.destination.model.transformers.SendDestinationPredefinedStateTransformer
|
||||
import com.tangem.features.send.subcomponents.destination.model.transformers.SendDestinationRecentListTransformer
|
||||
import com.tangem.features.send.subcomponents.destination.model.transformers.SendDestinationValidationResultTransformer
|
||||
import com.tangem.features.send.subcomponents.destination.model.transformers.SendDestinationValidationStartedTransformer
|
||||
import com.tangem.features.send.subcomponents.destination.model.transformers.*
|
||||
import com.tangem.features.send.subcomponents.destination.ui.state.DestinationWalletUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.subcomponents.destination.SendDestinationAlertFactory
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
|
|
@ -88,6 +85,11 @@ internal class SendDestinationModel @Inject constructor(
|
|||
private val getBackupProblematicWalletForAddressUseCase: GetBackupProblematicWalletForAddressUseCase,
|
||||
private val sendDestinationAlertFactory: SendDestinationAlertFactory,
|
||||
private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase,
|
||||
private val addressBookSendAnalytics: AddressBookSendAnalytics,
|
||||
private val syncAddressBooksUseCase: SyncAddressBooksUseCase,
|
||||
private val addressBookFeatureToggles: AddressBookFeatureToggles,
|
||||
getVerifiedContactsInteractor: GetVerifiedContactsInteractor,
|
||||
contactSelectionListener: ContactSelectionListener,
|
||||
) : Model(), SendDestinationClickIntents {
|
||||
private val params: SendDestinationComponentParams = paramsContainer.require()
|
||||
|
||||
|
|
@ -98,6 +100,39 @@ internal class SendDestinationModel @Inject constructor(
|
|||
private val cryptoCurrency = params.cryptoCurrency
|
||||
private val userWalletId = params.userWalletId
|
||||
|
||||
private val contacts: StateFlow<List<Contact>> =
|
||||
getVerifiedContactsInteractor.getVerifiedContacts(query = "", userWalletId = null)
|
||||
.map { verified -> verified.map { it.contact } }
|
||||
.flowOn(dispatchers.default)
|
||||
.stateIn(modelScope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
val addressSelectorNavigation = SlotNavigation<MatchedContact>()
|
||||
val addressQuery: StateFlow<String> = uiState
|
||||
.map { (it as? DestinationUM.Content)?.addressTextField?.value.orEmpty() }
|
||||
.distinctUntilChanged()
|
||||
.stateIn(modelScope, SharingStarted.Eagerly, "")
|
||||
|
||||
/**
|
||||
* Whether to offer saving the recipient to the address book under the recipient block. Only for the success-screen
|
||||
* block ([DestinationBlockParams.isAddContactAvailable]) and only when the recipient was NOT a contact and its
|
||||
* `(address, network)` pair is not already saved in the current wallet's book.
|
||||
*/
|
||||
val showAddContact: StateFlow<Boolean> =
|
||||
if ((params as? DestinationBlockParams)?.isAddContactAvailable == true) {
|
||||
combine(uiState, contacts) { state, contactList ->
|
||||
val address = (state as? DestinationUM.Content)?.addressTextField ?: return@combine false
|
||||
if (address.contactName != null) return@combine false // sent via a contact
|
||||
val networkId = cryptoCurrency.network.rawId
|
||||
contactList.none { contact ->
|
||||
contact.addresses.any {
|
||||
it.networkId.value == networkId && it.address.equals(address.actualAddress, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}.stateIn(modelScope, SharingStarted.Eagerly, false)
|
||||
} else {
|
||||
MutableStateFlow(false)
|
||||
}
|
||||
|
||||
// In "Send with swap" flow, these are addresses in the destination network (not the actual sender addresses).
|
||||
// Self-send validation must be skipped for them, so use only with params.isAllowSelfSend.
|
||||
private val senderAddresses = MutableStateFlow<List<CryptoCurrencyAddress>>(emptyList())
|
||||
|
|
@ -107,9 +142,62 @@ internal class SendDestinationModel @Inject constructor(
|
|||
private val backupProblematicWalletCache = AtomicReference<Pair<String, UserWalletId?>?>(null)
|
||||
|
||||
init {
|
||||
configDestinationNavigation()
|
||||
syncAddressBooksIfNeeded()
|
||||
subscribeOnQRScannerResult()
|
||||
initialState()
|
||||
resetContactOnEdit()
|
||||
contactSelectionListener.resultFlow
|
||||
.onEach(::applySelectedContact)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun resetContactOnEdit() {
|
||||
val params = params as? SendDestinationComponentParams.DestinationParams ?: return
|
||||
if (params.route.isEditMode) {
|
||||
val content = uiState.value as? DestinationUM.Content ?: return
|
||||
if (content.addressTextField.contactName != null) {
|
||||
_uiState.update(SendDestinationContactTransformer(contact = null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onContactClick(contact: MatchedContact) {
|
||||
val singleEntry = contact.entries.singleOrNull()
|
||||
if (singleEntry != null) {
|
||||
applySelectedContact(contact.toSelectedContact(singleEntry))
|
||||
} else {
|
||||
addressSelectorNavigation.activate(contact)
|
||||
}
|
||||
}
|
||||
|
||||
fun onSeeAllContactsClick() {
|
||||
router.push(
|
||||
AppRoute.AddressBook(AddressBookOpenMode.ContactSelection(networkId = cryptoCurrency.network.rawId)),
|
||||
)
|
||||
}
|
||||
|
||||
/** Opens the contact editor pre-filled with the sent address/network to save the recipient (success screen). */
|
||||
fun onAddContactClick() {
|
||||
val address = (uiState.value as? DestinationUM.Content)?.addressTextField?.actualAddress ?: return
|
||||
router.push(
|
||||
AppRoute.AddressBook(
|
||||
AddressBookOpenMode.WithContactCreation(address = address, networkId = cryptoCurrency.network.rawId),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun applySelectedContact(contact: SelectedContact) {
|
||||
addressSelectorNavigation.dismiss()
|
||||
addressBookSendAnalytics.onAddressSubstitutedInSend(walletId = userWalletId, contactId = contact.contactId)
|
||||
_uiState.update(SendDestinationAddressTransformer(address = contact.address, isPasted = true))
|
||||
_uiState.update(SendDestinationContactTransformer(contactName = contact.name, contactIcon = contact.icon))
|
||||
|
||||
val isMemoSupported = (uiState.value as? DestinationUM.Content)?.memoTextField != null
|
||||
val memo = contact.memo?.takeIf { isMemoSupported && it.isNotBlank() }
|
||||
if (memo != null) {
|
||||
_uiState.update(SendDestinationMemoTransformer(memo = memo, isPasted = true))
|
||||
}
|
||||
validate(address = contact.address, memo = memo, type = EnterAddressSource.Contact)
|
||||
}
|
||||
|
||||
private fun initialState() {
|
||||
|
|
@ -169,7 +257,23 @@ internal class SendDestinationModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun saveResult() {
|
||||
fun onBackClick() {
|
||||
val params = params as? SendDestinationComponentParams.DestinationParams ?: return
|
||||
if (!params.route.isEditMode) {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
source = SendScreenSource.Address,
|
||||
isFromSummary = false,
|
||||
isValid = uiState.value.isPrimaryButtonEnabled,
|
||||
),
|
||||
)
|
||||
saveResult()
|
||||
}
|
||||
params.callback.onBackClick(params.route)
|
||||
}
|
||||
|
||||
fun saveResult() {
|
||||
val params = params as? SendDestinationComponentParams.DestinationParams ?: return
|
||||
params.callback.onDestinationResult(uiState.value)
|
||||
}
|
||||
|
|
@ -186,6 +290,12 @@ internal class SendDestinationModel @Inject constructor(
|
|||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun syncAddressBooksIfNeeded() {
|
||||
if (addressBookFeatureToggles.isAddressBookEnabled) {
|
||||
modelScope.launch(context = dispatchers.default) { syncAddressBooksUseCase() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeOnQRScannerResult() {
|
||||
listenToQrScanningUseCase(SourceType.SEND)
|
||||
.getOrElse { emptyFlow() }
|
||||
|
|
@ -371,6 +481,7 @@ internal class SendDestinationModel @Inject constructor(
|
|||
isMemoRequired = isMemoRequired,
|
||||
),
|
||||
)
|
||||
recognizeContact(type = type, isValidAddress = addressValidationResult.isRight(), address = resolvedAddress)
|
||||
if (type != null) {
|
||||
autoNextFromRecipient(
|
||||
type = type,
|
||||
|
|
@ -381,61 +492,26 @@ internal class SendDestinationModel @Inject constructor(
|
|||
}.saveIn(validationJobHolder)
|
||||
}
|
||||
|
||||
private fun autoNextFromRecipient(type: EnterAddressSource, isValidAddress: Boolean, isValidMemo: Boolean) {
|
||||
if (type.isAutoNext && isValidAddress && isValidMemo) {
|
||||
saveResult()
|
||||
(params as? SendDestinationComponentParams.DestinationParams)?.callback?.onNextClick()
|
||||
private fun recognizeContact(type: EnterAddressSource?, isValidAddress: Boolean, address: String) {
|
||||
if (type == null || type == EnterAddressSource.Contact) return
|
||||
val contact = if (isValidAddress) findContactByAddress(address) else null
|
||||
_uiState.update(SendDestinationContactTransformer(contact))
|
||||
}
|
||||
|
||||
private fun findContactByAddress(address: String): Contact? {
|
||||
val networkId = cryptoCurrency.network.rawId
|
||||
return contacts.value.firstOrNull { contact ->
|
||||
contact.addresses.any { entry ->
|
||||
entry.networkId.value == networkId && entry.address.equals(address, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun configDestinationNavigation() {
|
||||
val params = params as? SendDestinationComponentParams.DestinationParams ?: return
|
||||
combine(
|
||||
flow = uiState,
|
||||
flow2 = params.currentRoute,
|
||||
transform = { state, route -> state to route },
|
||||
).onEach { (state, route) ->
|
||||
params.callback.onNavigationResult(
|
||||
NavigationUM.Content(
|
||||
source = CommonSendRoute.Destination::class.java.simpleName,
|
||||
title = params.title,
|
||||
subtitle = null,
|
||||
backIconRes = if (route.isEditMode) {
|
||||
R.drawable.ic_back_24
|
||||
} else {
|
||||
R.drawable.ic_close_24
|
||||
},
|
||||
backIconClick = {
|
||||
if (!route.isEditMode) {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.CloseButtonClicked(
|
||||
categoryName = params.analyticsCategoryName,
|
||||
source = SendScreenSource.Address,
|
||||
isFromSummary = false,
|
||||
isValid = state.isPrimaryButtonEnabled,
|
||||
),
|
||||
)
|
||||
saveResult()
|
||||
}
|
||||
params.callback.onBackClick()
|
||||
},
|
||||
primaryButton = NavigationButton(
|
||||
textReference = if (route.isEditMode) {
|
||||
resourceReference(R.string.common_continue)
|
||||
} else {
|
||||
resourceReference(R.string.common_next)
|
||||
},
|
||||
isEnabled = state.isPrimaryButtonEnabled,
|
||||
onClick = {
|
||||
saveResult()
|
||||
params.callback.onNextClick()
|
||||
},
|
||||
),
|
||||
secondaryPairButtonsUM = null,
|
||||
),
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
private fun autoNextFromRecipient(type: EnterAddressSource, isValidAddress: Boolean, isValidMemo: Boolean) {
|
||||
if (type.isAutoNext && isValidAddress && isValidMemo) {
|
||||
saveResult()
|
||||
(params as? SendDestinationComponentParams.DestinationParams)?.callback?.onNextClick(params.route)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.send.subcomponents.destination.model.converter
|
||||
|
||||
import com.tangem.common.ui.account.AccountIconUM
|
||||
import com.tangem.domain.addressbook.model.Contact
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal object ContactIconConverter : Converter<Contact, AccountIconUM.CryptoPortfolio> {
|
||||
|
||||
override fun convert(value: Contact): AccountIconUM.CryptoPortfolio = AccountIconUM.CryptoPortfolio(
|
||||
value = CryptoPortfolioIcon.Icon.Letter,
|
||||
color = CryptoPortfolioIcon.Color.entries.firstOrNull { it.name == value.iconColor }
|
||||
?: CryptoPortfolioIcon.Color.Azure,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.features.send.subcomponents.destination.model.transformers
|
||||
|
||||
import com.tangem.common.ui.account.AccountIconUM
|
||||
import com.tangem.domain.addressbook.model.Contact
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.subcomponents.destination.model.converter.ContactIconConverter
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SendDestinationContactTransformer(
|
||||
private val contactName: String?,
|
||||
private val contactIcon: AccountIconUM.CryptoPortfolio?,
|
||||
) : Transformer<DestinationUM> {
|
||||
|
||||
constructor(contact: Contact?) : this(
|
||||
contactName = contact?.name?.value,
|
||||
contactIcon = contact?.let(ContactIconConverter::convert),
|
||||
)
|
||||
|
||||
override fun transform(prevState: DestinationUM): DestinationUM {
|
||||
val state = prevState as? DestinationUM.Content ?: return prevState
|
||||
|
||||
return state.copy(
|
||||
addressTextField = state.addressTextField.copy(
|
||||
contactName = contactName,
|
||||
contactIcon = contactIcon,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -15,17 +15,21 @@ import androidx.compose.ui.platform.testTag
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.common.ui.account.AccountIcon
|
||||
import com.tangem.core.ui.components.SecondaryButtonIconStart
|
||||
import com.tangem.core.ui.components.account.AccountIconSize
|
||||
import com.tangem.core.ui.components.icons.identicon.IdentIcon
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.SendConfirmScreenTestTags
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationRecipientListUM
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationTextFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.impl.R
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
|
|
@ -34,6 +38,8 @@ internal fun DestinationBlock(
|
|||
isClickDisabled: Boolean,
|
||||
isEditingDisabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
showAddContact: Boolean = false,
|
||||
onAddContactClick: () -> Unit = {},
|
||||
) {
|
||||
if (destinationUM !is DestinationUM.Content) return
|
||||
|
||||
|
|
@ -50,6 +56,16 @@ internal fun DestinationBlock(
|
|||
address = destinationUM.addressTextField,
|
||||
memo = destinationUM.memoTextField,
|
||||
)
|
||||
if (showAddContact) {
|
||||
SecondaryButtonIconStart(
|
||||
text = stringResourceSafe(com.tangem.core.ui.R.string.address_book_add_contact),
|
||||
iconResId = com.tangem.core.ui.R.drawable.ic_plus_24,
|
||||
onClick = onAddContactClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -68,6 +84,8 @@ private fun AddressWithMemoBlock(
|
|||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24),
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
val contactName = address.contactName
|
||||
val contactIcon = address.contactIcon
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = address.value,
|
||||
|
|
@ -85,14 +103,23 @@ private fun AddressWithMemoBlock(
|
|||
)
|
||||
}
|
||||
}
|
||||
IdentIcon(
|
||||
address = address.value,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size36)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius18))
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.testTag(SendConfirmScreenTestTags.RECIPIENT_ADDRESS_ICON),
|
||||
)
|
||||
if (contactName != null && contactIcon != null) {
|
||||
AccountIcon(
|
||||
name = stringReference(contactName),
|
||||
icon = contactIcon,
|
||||
size = AccountIconSize.ContactDefault,
|
||||
modifier = Modifier.testTag(SendConfirmScreenTestTags.RECIPIENT_ADDRESS_ICON),
|
||||
)
|
||||
} else {
|
||||
IdentIcon(
|
||||
address = address.value,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size36)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius18))
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.testTag(SendConfirmScreenTestTags.RECIPIENT_ADDRESS_ICON),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (memo != null && memo.value.isNotBlank()) {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import com.tangem.core.ui.extensions.*
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.test.SendAddressScreenTestTags
|
||||
import com.tangem.core.ui.utils.GlobalMultipleClickPreventer
|
||||
import com.tangem.features.addressbook.AddressBookContactsBlockComponent
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationRecipientListUM
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationTextFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
|
|
@ -47,6 +48,7 @@ internal fun SendDestinationContent(
|
|||
state: DestinationUM,
|
||||
clickIntents: SendDestinationClickIntents,
|
||||
isBalanceHidden: Boolean,
|
||||
contactsBlock: AddressBookContactsBlockComponent? = null,
|
||||
) {
|
||||
if (state !is DestinationUM.Content) return
|
||||
val recipients = state.recent
|
||||
|
|
@ -75,6 +77,15 @@ internal fun SendDestinationContent(
|
|||
onMemoChange = clickIntents::onRecipientMemoValueChange,
|
||||
)
|
||||
}
|
||||
if (contactsBlock != null && !state.isRecentHidden) {
|
||||
item(key = "CONTACTS_BLOCK_KEY") {
|
||||
contactsBlock.Content(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
listHeaderItem(
|
||||
titleRes = if (state.isAccountsMode == true) {
|
||||
R.string.common_accounts
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.send.subcomponents.fee.model.converters.custom
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.features.send.api.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.CustomFeeFieldUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import com.tangem.core.ui.utils.parseBigDecimal
|
|||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.api.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkExceedBalance
|
||||
import com.tangem.features.send.subcomponents.fee.model.converters.custom.CustomFeeConverter
|
||||
import com.tangem.features.send.impl.R
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.features.send.subcomponents.fee.model.converters.custom.ether
|
|||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.features.send.subcomponents.fee.model.converters.custom.CustomFeeConverter
|
||||
import com.tangem.features.send.api.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.CustomFeeFieldUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.api.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkExceedBalance
|
||||
import com.tangem.features.send.impl.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import com.tangem.core.ui.utils.parseBigDecimal
|
|||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.api.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkExceedBalance
|
||||
import com.tangem.features.send.subcomponents.fee.model.converters.custom.setEmpty
|
||||
import com.tangem.features.send.impl.R
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import com.tangem.core.ui.utils.parseBigDecimal
|
|||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.api.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkExceedBalance
|
||||
import com.tangem.features.send.subcomponents.fee.model.converters.custom.setEmpty
|
||||
import com.tangem.features.send.impl.R
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import com.tangem.core.ui.utils.parseBigDecimal
|
|||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.api.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.subcomponents.fee.model.converters.custom.CustomFeeConverter
|
||||
import com.tangem.features.send.impl.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.send.subcomponents.notifications
|
||||
|
||||
import com.tangem.features.send.api.SendNotificationsComponent.Params.NotificationData
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent.Params.NotificationData
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateListener
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateTrigger
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import androidx.compose.ui.Modifier
|
|||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.send.api.SendNotificationsComponent
|
||||
import com.tangem.features.send.api.SendNotificationsComponent.Params
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent.Params
|
||||
import com.tangem.features.send.subcomponents.notifications
|
||||
import com.tangem.features.send.subcomponents.notifications.model.NotificationsModel
|
||||
import dagger.assisted.Assisted
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
|
|||
import com.tangem.domain.transaction.models.AssetRequirementsCondition
|
||||
import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.features.send.api.SendNotificationsComponent
|
||||
import com.tangem.features.send.api.SendNotificationsComponent.Params.NotificationData
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsComponent.Params.NotificationData
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkAndCalculateSubtractedAmount
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkFeeCoverage
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateListener
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.features.send
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Builds a [TestingCoroutineDispatcherProvider] backed by a single [StandardTestDispatcher] wired to this scope's
|
||||
* [TestScope.testScheduler], so `advanceUntilIdle()` drives all five dispatcher roles. Use in `Model`-layer tests
|
||||
* instead of copying the wiring per file.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal fun TestScope.testDispatcherProvider(): TestingCoroutineDispatcherProvider {
|
||||
val testDispatcher = StandardTestDispatcher(testScheduler)
|
||||
return TestingCoroutineDispatcherProvider(
|
||||
main = testDispatcher,
|
||||
mainImmediate = testDispatcher,
|
||||
io = testDispatcher,
|
||||
default = testDispatcher,
|
||||
single = testDispatcher,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared `Loaded` status fixture for send-impl tests. Only [currency], [fiatRate] and [balance] differ between
|
||||
* call sites; the rest is incidental and never asserted.
|
||||
*/
|
||||
internal fun loadedStatus(
|
||||
currency: CryptoCurrency,
|
||||
fiatRate: BigDecimal = BigDecimal.ONE,
|
||||
balance: BigDecimal = BigDecimal.ONE,
|
||||
): CryptoCurrencyStatus = CryptoCurrencyStatus(
|
||||
currency = currency,
|
||||
value = CryptoCurrencyStatus.Loaded(
|
||||
amount = balance,
|
||||
fiatAmount = fiatRate,
|
||||
fiatRate = fiatRate,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
stakingBalance = null,
|
||||
yieldSupplyStatus = null,
|
||||
hasCurrentNetworkTransactions = false,
|
||||
pendingTransactions = emptySet(),
|
||||
networkAddress = NetworkAddress.Single(
|
||||
NetworkAddress.Address(value = "address", type = NetworkAddress.Address.Type.Primary),
|
||||
),
|
||||
sources = CryptoCurrencyStatus.Sources(),
|
||||
),
|
||||
)
|
||||
|
||||
/** Throwaway [Fee.Common] for tests that only need "some fee" of a non-special type. */
|
||||
internal fun commonFee(blockchain: Blockchain = Blockchain.Ethereum): Fee.Common = Fee.Common(Amount(blockchain))
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
package com.tangem.features.send.feeselector.model
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils
|
||||
import com.tangem.features.send.commonFee
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.unmockkObject
|
||||
import io.mockk.verify
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class FeeSelectorAlertFactoryTest {
|
||||
|
||||
private val messageSender: UiMessageSender = mockk(relaxed = true)
|
||||
private val factory = FeeSelectorAlertFactory(messageSender)
|
||||
|
||||
@BeforeEach
|
||||
fun resetSender() {
|
||||
clearMocks(messageSender)
|
||||
}
|
||||
|
||||
private fun ethFee(value: String): Fee =
|
||||
Fee.Common(Amount(currencySymbol = "ETH", value = BigDecimal(value), decimals = 18))
|
||||
|
||||
private fun content(selected: FeeItem) = FeeSelectorUM.Content(
|
||||
isPrimaryButtonEnabled = true,
|
||||
fees = TransactionFee.Single(normal = commonFee()),
|
||||
feeItems = persistentListOf(selected),
|
||||
selectedFeeItem = selected,
|
||||
feeExtraInfo = mockk(),
|
||||
feeFiatRateUM = null,
|
||||
feeNonce = FeeNonce.None,
|
||||
)
|
||||
|
||||
private fun choosable(normal: Fee, minimum: Fee, priority: Fee) =
|
||||
TransactionFee.Choosable(normal = normal, minimum = minimum, priority = priority)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class GetFeeUpdatedAlert {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `GIVEN reloaded fee WHEN getFeeUpdatedAlert THEN resolves to warn proceed or nothing`(model: UpdatedModel) {
|
||||
// Arrange
|
||||
val proceed: () -> Unit = mockk(relaxed = true)
|
||||
|
||||
// Act
|
||||
factory.getFeeUpdatedAlert(
|
||||
model.newFee,
|
||||
model.state,
|
||||
proceedAction = proceed,
|
||||
stopAction = mockk(relaxed = true),
|
||||
)
|
||||
|
||||
// Assert
|
||||
verify(exactly = if (model.outcome == Outcome.DIALOG) 1 else 0) { messageSender.send(any()) }
|
||||
verify(exactly = if (model.outcome == Outcome.PROCEED) 1 else 0) { proceed() }
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
// Market -> normal, higher -> warn
|
||||
UpdatedModel(
|
||||
content(FeeItem.Market(ethFee("1"))),
|
||||
choosable(ethFee("2"), ethFee("0"), ethFee("0")),
|
||||
Outcome.DIALOG
|
||||
),
|
||||
// Market -> normal, not higher -> proceed
|
||||
UpdatedModel(
|
||||
content(FeeItem.Market(ethFee("2"))),
|
||||
choosable(ethFee("1"), ethFee("0"), ethFee("0")),
|
||||
Outcome.PROCEED
|
||||
),
|
||||
// Slow -> minimum
|
||||
UpdatedModel(
|
||||
content(FeeItem.Slow(ethFee("1"))),
|
||||
choosable(ethFee("0"), ethFee("2"), ethFee("0")),
|
||||
Outcome.DIALOG
|
||||
),
|
||||
// Fast -> priority
|
||||
UpdatedModel(
|
||||
content(FeeItem.Fast(ethFee("1"))),
|
||||
choosable(ethFee("0"), ethFee("0"), ethFee("2")),
|
||||
Outcome.DIALOG
|
||||
),
|
||||
// Single -> normal
|
||||
UpdatedModel(
|
||||
content(FeeItem.Market(ethFee("1"))),
|
||||
TransactionFee.Single(ethFee("2")),
|
||||
Outcome.DIALOG
|
||||
),
|
||||
// Suggested -> its own fee == old fee, never higher -> proceed
|
||||
UpdatedModel(
|
||||
content(FeeItem.Suggested(title = mockk(), fee = ethFee("5"))),
|
||||
choosable(ethFee("9"), ethFee("9"), ethFee("9")),
|
||||
Outcome.PROCEED,
|
||||
),
|
||||
// Custom selected -> early return, nothing happens
|
||||
UpdatedModel(
|
||||
content(FeeItem.Custom(fee = ethFee("1"), customValues = persistentListOf())),
|
||||
choosable(ethFee("9"), ethFee("9"), ethFee("9")),
|
||||
Outcome.NOTHING,
|
||||
),
|
||||
// non-content state -> early return, nothing happens
|
||||
UpdatedModel(
|
||||
FeeSelectorUM.Loading,
|
||||
TransactionFee.Single(ethFee("2")),
|
||||
Outcome.NOTHING
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class CheckAndShowAlerts {
|
||||
|
||||
@BeforeEach
|
||||
fun mockUtils() {
|
||||
mockkObject(FeeCalculationUtils)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun unmockUtils() {
|
||||
unmockkObject(FeeCalculationUtils)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `GIVEN fee validity WHEN checkAndShowAlerts THEN confirms only when no alert shown`(model: AlertsModel) {
|
||||
// Arrange
|
||||
every { FeeCalculationUtils.checkIfCustomFeeTooLow(any()) } returns model.tooLow
|
||||
every { FeeCalculationUtils.checkIfCustomFeeTooHigh(any()) } returns (model.tooHigh to "5")
|
||||
val onConfirm: () -> Unit = mockk(relaxed = true)
|
||||
|
||||
// Act
|
||||
factory.checkAndShowAlerts(content(FeeItem.Market(ethFee("1"))), onConfirm)
|
||||
|
||||
// Assert
|
||||
verify(exactly = model.expectedSends) { messageSender.send(any()) }
|
||||
verify(exactly = if (model.expectConfirm) 1 else 0) { onConfirm() }
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
AlertsModel(tooLow = false, tooHigh = false, expectedSends = 0, expectConfirm = true),
|
||||
AlertsModel(tooLow = true, tooHigh = false, expectedSends = 1, expectConfirm = false),
|
||||
AlertsModel(tooLow = false, tooHigh = true, expectedSends = 1, expectConfirm = false),
|
||||
)
|
||||
}
|
||||
|
||||
enum class Outcome { DIALOG, PROCEED, NOTHING }
|
||||
|
||||
data class UpdatedModel(val state: FeeSelectorUM, val newFee: TransactionFee, val outcome: Outcome)
|
||||
data class AlertsModel(
|
||||
val tooLow: Boolean,
|
||||
val tooHigh: Boolean,
|
||||
val expectedSends: Int,
|
||||
val expectConfirm: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
package com.tangem.features.send.feeselector.model
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.GetAvailableFeeTokensUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams.FeeStateConfiguration
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorCheckReloadListener
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorCheckReloadTrigger
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorReloadListener
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents
|
||||
import com.tangem.features.send.loadedStatus
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import java.math.BigDecimal
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class FeeSelectorLogicTest {
|
||||
|
||||
private val testUserWalletId = UserWalletId("1234567890ABCDEF")
|
||||
private val coinStatus: CryptoCurrencyStatus = loadedStatus(mockk<CryptoCurrency.Coin>(relaxed = true))
|
||||
private val tokenStatus: CryptoCurrencyStatus = loadedStatus(mockk<CryptoCurrency.Token>(relaxed = true))
|
||||
|
||||
private val isFeeApproximateUseCase: IsFeeApproximateUseCase = mockk(relaxed = true)
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase = mockk(relaxed = true)
|
||||
private val feeSelectorReloadListener: FeeSelectorReloadListener = mockk(relaxed = true)
|
||||
private val feeSelectorCheckReloadListener: FeeSelectorCheckReloadListener = mockk(relaxed = true)
|
||||
private val feeSelectorCheckReloadTrigger: FeeSelectorCheckReloadTrigger = mockk(relaxed = true)
|
||||
private val feeSelectorAlertFactory: FeeSelectorAlertFactory = mockk(relaxed = true)
|
||||
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier = mockk(relaxed = true)
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase = mockk(relaxed = true)
|
||||
private val getAvailableFeeTokensUseCase: GetAvailableFeeTokensUseCase = mockk(relaxed = true)
|
||||
private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk(relaxed = true)
|
||||
|
||||
private val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee> = mockk()
|
||||
private val onLoadFeeExtended: suspend (CryptoCurrencyStatus?) -> Either<GetFeeError, TransactionFeeExtended> =
|
||||
mockk()
|
||||
|
||||
private val checkReloadTriggerFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
MockKAnnotations.init(this)
|
||||
// PER_CLASS parameterized nested classes reuse one instance — reset analytics recorded calls between rows.
|
||||
clearMocks(analyticsEventHandler, answers = false, recordedCalls = true, childMocks = false)
|
||||
coEvery { onLoadFee() } returns GetFeeError.UnknownError.left()
|
||||
coEvery { onLoadFeeExtended(any()) } returns GetFeeError.UnknownError.left()
|
||||
coEvery { getSelectedAppCurrencyUseCase.invokeSync() } returns AppCurrency.Default.right()
|
||||
every { feeSelectorReloadListener.reloadTriggerFlow } returns emptyFlow()
|
||||
every { feeSelectorReloadListener.loadingStateTriggerFlow } returns emptyFlow()
|
||||
every { feeSelectorCheckReloadListener.checkReloadTriggerFlow } returns checkReloadTriggerFlow
|
||||
every { isGaslessFeeSupportedForNetwork(any()) } returns false
|
||||
every { isFeeApproximateUseCase(any(), any()) } returns false
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class CallLoadFee {
|
||||
|
||||
@Test
|
||||
fun `GIVEN gasless disabled WHEN load fee THEN use basic onLoadFee only`() =
|
||||
runTest(UnconfinedTestDispatcher()) {
|
||||
// Act — init triggers loadFee()
|
||||
buildModel(gaslessEnabled = false)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { onLoadFee() }
|
||||
coVerify(exactly = 0) { onLoadFeeExtended(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN gasless not enough funds WHEN load fee THEN surface error without basic fallback`() =
|
||||
runTest(UnconfinedTestDispatcher()) {
|
||||
// Arrange
|
||||
coEvery { onLoadFeeExtended(any()) } returns GetFeeError.GaslessError.NotEnoughFunds.left()
|
||||
|
||||
// Act
|
||||
val sut = buildModel(gaslessEnabled = true)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { onLoadFeeExtended(any()) }
|
||||
coVerify(exactly = 0) { onLoadFee() }
|
||||
assertThat(sut.uiState.value).isInstanceOf(FeeSelectorUM.Error::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN gasless generic error WHEN load fee THEN fallback to basic and show only speed option`() =
|
||||
runTest(UnconfinedTestDispatcher()) {
|
||||
// Arrange
|
||||
coEvery { onLoadFeeExtended(any()) } returns GetFeeError.GaslessError.NetworkIsNotSupported.left()
|
||||
coEvery { onLoadFee() } returns GetFeeError.UnknownError.left()
|
||||
|
||||
// Act
|
||||
val sut = buildModel(gaslessEnabled = true)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { onLoadFeeExtended(any()) }
|
||||
coVerify(exactly = 1) { onLoadFee() }
|
||||
assertThat(sut.shouldShowOnlySpeedOption.value).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN gasless success WHEN load fee THEN use extended and clear speed-only option`() =
|
||||
runTest(UnconfinedTestDispatcher()) {
|
||||
// Arrange — populateExtendedFee then fails (token not found) but the dispatch decision is already made
|
||||
val feeExtended = TransactionFeeExtended(
|
||||
transactionFee = singleFee(),
|
||||
feeTokenId = mockk(relaxed = true), // != feeCryptoCurrencyStatus.currency.id -> token lookup
|
||||
)
|
||||
coEvery { onLoadFeeExtended(any()) } returns feeExtended.right()
|
||||
coEvery { singleAccountStatusListSupplier.getSyncOrNull(any<UserWalletId>()) } returns null
|
||||
|
||||
// Act
|
||||
val sut = buildModel(gaslessEnabled = true)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { onLoadFeeExtended(any()) }
|
||||
assertThat(sut.shouldShowOnlySpeedOption.value).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class CheckLoadFee {
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee reloads successfully WHEN check requested THEN show fee-updated alert`() =
|
||||
runTest(UnconfinedTestDispatcher()) {
|
||||
// Arrange
|
||||
coEvery { onLoadFee() } returns singleFee().right()
|
||||
buildModel(gaslessEnabled = false)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
checkReloadTriggerFlow.tryEmit(Unit)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
verify(atLeast = 1) { feeSelectorAlertFactory.getFeeUpdatedAlert(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fee reload fails WHEN check requested THEN report failure and show unreachable error`() =
|
||||
runTest(UnconfinedTestDispatcher()) {
|
||||
// Arrange
|
||||
coEvery { onLoadFee() } returns GetFeeError.UnknownError.left()
|
||||
buildModel(gaslessEnabled = false)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
checkReloadTriggerFlow.tryEmit(Unit)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(atLeast = 1) { feeSelectorCheckReloadTrigger.callbackCheckResult(false) }
|
||||
verify(atLeast = 1) { feeSelectorAlertFactory.getFeeUnreachableErrorState(any()) }
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class OnFeeItemSelected {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `WHEN fee item selected THEN send custom-fee analytics only for custom`(model: FeeItemSelectedModel) =
|
||||
runTest(UnconfinedTestDispatcher()) {
|
||||
// Arrange
|
||||
val sut = buildModel(gaslessEnabled = false)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
sut.onFeeItemSelected(model.feeItem)
|
||||
|
||||
// Assert
|
||||
verify(exactly = model.expectedAnalyticsCalls) {
|
||||
analyticsEventHandler.send(ofType<CommonSendFeeAnalyticEvents.CustomFeeButtonClicked>())
|
||||
}
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
FeeItemSelectedModel(
|
||||
feeItem = FeeItem.Custom(fee = realFee(), customValues = persistentListOf()),
|
||||
expectedAnalyticsCalls = 1,
|
||||
),
|
||||
FeeItemSelectedModel(feeItem = FeeItem.Market(fee = realFee()), expectedAnalyticsCalls = 0),
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class OnDoneClick {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `WHEN done THEN always send selected-fee and gas-price only for edited custom`(model: DoneClickModel) =
|
||||
runTest(UnconfinedTestDispatcher()) {
|
||||
// Arrange
|
||||
val sut = buildModel(gaslessEnabled = false)
|
||||
advanceUntilIdle()
|
||||
sut.uiState.value = contentState(selected = model.selected, normalValue = model.normalValue)
|
||||
|
||||
// Act
|
||||
sut.onDoneClick()
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) { analyticsEventHandler.send(ofType<CommonSendFeeAnalyticEvents.SelectedFee>()) }
|
||||
verify(exactly = model.expectedGasPriceCalls) { analyticsEventHandler.send(ofType<CommonSendFeeAnalyticEvents.GasPriceInserter>()) }
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
// not custom -> no gas-price
|
||||
DoneClickModel(
|
||||
selected = FeeItem.Market(realFee("0.001")),
|
||||
normalValue = "0.001",
|
||||
expectedGasPriceCalls = 0
|
||||
),
|
||||
// custom but unedited (== normal) -> no gas-price
|
||||
DoneClickModel(
|
||||
selected = FeeItem.Custom(realFee("0.001"), persistentListOf()),
|
||||
normalValue = "0.001",
|
||||
expectedGasPriceCalls = 0,
|
||||
),
|
||||
// custom edited (!= normal) -> gas-price
|
||||
DoneClickModel(
|
||||
selected = FeeItem.Custom(realFee("0.005"), persistentListOf()),
|
||||
normalValue = "0.001",
|
||||
expectedGasPriceCalls = 1,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// region fixtures
|
||||
|
||||
private fun TestScope.buildModel(gaslessEnabled: Boolean): FeeSelectorLogic {
|
||||
val currencyStatus = if (gaslessEnabled) tokenStatus else coinStatus
|
||||
every { isGaslessFeeSupportedForNetwork(any()) } returns gaslessEnabled
|
||||
val params = FeeSelectorParams.FeeSelectorBlockParams(
|
||||
state = FeeSelectorUM.Loading,
|
||||
userWalletId = testUserWalletId,
|
||||
onLoadFeeExtended = if (gaslessEnabled) onLoadFeeExtended else null,
|
||||
onLoadFee = onLoadFee,
|
||||
cryptoCurrencyStatus = currencyStatus,
|
||||
feeCryptoCurrencyStatus = currencyStatus,
|
||||
feeStateConfiguration = FeeStateConfiguration.None,
|
||||
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.BottomSheet,
|
||||
analyticsCategoryName = "test_fee",
|
||||
analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.Send,
|
||||
)
|
||||
return FeeSelectorLogic(
|
||||
params = params,
|
||||
modelScope = backgroundScope,
|
||||
isFeeApproximateUseCase = isFeeApproximateUseCase,
|
||||
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
|
||||
feeSelectorReloadListener = feeSelectorReloadListener,
|
||||
feeSelectorCheckReloadListener = feeSelectorCheckReloadListener,
|
||||
feeSelectorCheckReloadTrigger = feeSelectorCheckReloadTrigger,
|
||||
feeSelectorAlertFactory = feeSelectorAlertFactory,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
getUserWalletUseCase = getUserWalletUseCase,
|
||||
getAvailableFeeTokensUseCase = getAvailableFeeTokensUseCase,
|
||||
isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork,
|
||||
)
|
||||
}
|
||||
|
||||
private fun contentState(selected: FeeItem, normalValue: String): FeeSelectorUM.Content {
|
||||
val extraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = true,
|
||||
isTronToken = false,
|
||||
feeCryptoCurrencyStatus = coinStatus,
|
||||
)
|
||||
return FeeSelectorUM.Content(
|
||||
isPrimaryButtonEnabled = true,
|
||||
fees = singleFee(normalValue),
|
||||
feeItems = persistentListOf(selected),
|
||||
selectedFeeItem = selected,
|
||||
feeExtraInfo = extraInfo,
|
||||
feeFiatRateUM = null,
|
||||
feeNonce = FeeNonce.None,
|
||||
)
|
||||
}
|
||||
|
||||
private fun realFee(value: String = "0.001"): Fee = Fee.Common(
|
||||
Amount(currencySymbol = "ETH", value = BigDecimal(value), decimals = 18),
|
||||
)
|
||||
|
||||
private fun singleFee(value: String = "0.001"): TransactionFee = TransactionFee.Single(normal = realFee(value))
|
||||
|
||||
data class FeeItemSelectedModel(val feeItem: FeeItem, val expectedAnalyticsCalls: Int)
|
||||
|
||||
data class DoneClickModel(val selected: FeeItem, val normalValue: String, val expectedGasPriceCalls: Int)
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
package com.tangem.features.send.feeselector.model.transformers
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams
|
||||
import com.tangem.features.send.commonFee
|
||||
import com.tangem.features.send.loadedStatus
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class FeeItemConverterTest {
|
||||
|
||||
// Bitcoin status so the custom-fee field converter yields fields for a Bitcoin normalFee.
|
||||
private val feeStatus = loadedStatus(
|
||||
currency = MockCryptoCurrencyFactory().createCoin(Blockchain.Bitcoin),
|
||||
fiatRate = BigDecimal("50000"),
|
||||
)
|
||||
|
||||
private val bitcoinFee: Fee = Fee.Bitcoin(
|
||||
amount = Amount(currencySymbol = "BTC", value = BigDecimal("0.0001"), decimals = 8),
|
||||
satoshiPerByte = BigDecimal("10"),
|
||||
txSize = BigDecimal("250"),
|
||||
)
|
||||
|
||||
private fun converter(
|
||||
config: FeeSelectorParams.FeeStateConfiguration = FeeSelectorParams.FeeStateConfiguration.None,
|
||||
normalFee: Fee = commonFee(),
|
||||
shouldDisableCustomFee: Boolean = true,
|
||||
) = FeeItemConverter(
|
||||
feeStateConfiguration = config,
|
||||
normalFee = normalFee,
|
||||
feeSelectorIntents = mockk(relaxed = true),
|
||||
appCurrency = AppCurrency.Default,
|
||||
cryptoCurrencyStatus = feeStatus,
|
||||
shouldDisableCustomFee = shouldDisableCustomFee,
|
||||
)
|
||||
|
||||
private fun choosable() =
|
||||
TransactionFee.Choosable(normal = commonFee(), minimum = commonFee(), priority = commonFee())
|
||||
|
||||
private fun single() = TransactionFee.Single(normal = commonFee())
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Items {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `GIVEN config and transaction fee WHEN convert THEN fee items match configuration`(model: ItemsModel) {
|
||||
// Act (custom fee disabled -> the list is purely config driven)
|
||||
val actual = converter(config = model.config)
|
||||
.convert(FeeItemConverter.Input(transactionFee = model.transactionFee, customFee = null))
|
||||
|
||||
// Assert
|
||||
assertThat(actual.map { it::class.java }).containsExactlyElementsIn(model.expectedTypes).inOrder()
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
ItemsModel(
|
||||
none(),
|
||||
choosable(),
|
||||
listOf(FeeItem.Slow::class.java, FeeItem.Market::class.java, FeeItem.Fast::class.java)
|
||||
),
|
||||
ItemsModel(none(), single(), listOf(FeeItem.Market::class.java)),
|
||||
ItemsModel(
|
||||
suggestion(),
|
||||
choosable(),
|
||||
listOf(
|
||||
FeeItem.Suggested::class.java,
|
||||
FeeItem.Slow::class.java,
|
||||
FeeItem.Market::class.java,
|
||||
FeeItem.Fast::class.java
|
||||
),
|
||||
),
|
||||
ItemsModel(
|
||||
suggestion(),
|
||||
single(),
|
||||
listOf(FeeItem.Suggested::class.java, FeeItem.Market::class.java)
|
||||
),
|
||||
ItemsModel(
|
||||
excludeLow(),
|
||||
choosable(),
|
||||
listOf(FeeItem.Market::class.java, FeeItem.Fast::class.java)
|
||||
),
|
||||
ItemsModel(
|
||||
excludeLow(),
|
||||
single(),
|
||||
listOf(FeeItem.Market::class.java)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class FeeAssignment {
|
||||
|
||||
@Test
|
||||
fun `GIVEN choosable fee WHEN convert THEN slow market fast map to minimum normal priority`() {
|
||||
// Arrange (distinct fees to detect any mis-mapping)
|
||||
val minimum = ethFee(value = "1")
|
||||
val normal = ethFee(value = "2")
|
||||
val priority = ethFee(value = "3")
|
||||
val fees = TransactionFee.Choosable(normal = normal, minimum = minimum, priority = priority)
|
||||
|
||||
// Act
|
||||
val actual = converter(config = none()).convert(FeeItemConverter.Input(fees, customFee = null))
|
||||
|
||||
// Assert
|
||||
assertThat((actual[0] as FeeItem.Slow).fee).isEqualTo(minimum)
|
||||
assertThat((actual[1] as FeeItem.Market).fee).isEqualTo(normal)
|
||||
assertThat((actual[2] as FeeItem.Fast).fee).isEqualTo(priority)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class CustomFee {
|
||||
|
||||
@Test
|
||||
fun `GIVEN custom enabled and supported fee WHEN convert THEN custom fee appended`() {
|
||||
// Act
|
||||
val actual = converter(normalFee = bitcoinFee, shouldDisableCustomFee = false)
|
||||
.convert(FeeItemConverter.Input(TransactionFee.Single(bitcoinFee), customFee = null))
|
||||
|
||||
// Assert
|
||||
assertThat(actual).hasSize(2) // Market + Custom
|
||||
assertThat(actual.last()).isInstanceOf(FeeItem.Custom::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN custom disabled WHEN convert THEN no custom fee`() {
|
||||
// Act
|
||||
val actual = converter(normalFee = bitcoinFee, shouldDisableCustomFee = true)
|
||||
.convert(FeeItemConverter.Input(TransactionFee.Single(bitcoinFee), customFee = null))
|
||||
|
||||
// Assert
|
||||
assertThat(actual).hasSize(1) // Market only
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN unsupported fee with no custom fields WHEN convert THEN no custom fee`() {
|
||||
// Act (Fee.Common has no custom field converter -> constructCustomFee returns null)
|
||||
val actual = converter(normalFee = commonFee(), shouldDisableCustomFee = false)
|
||||
.convert(FeeItemConverter.Input(TransactionFee.Single(commonFee()), customFee = null))
|
||||
|
||||
// Assert
|
||||
assertThat(actual).hasSize(1) // Market only
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN custom fee provided WHEN convert THEN provided custom reused`() {
|
||||
// Arrange
|
||||
val provided = FeeItem.Custom(fee = bitcoinFee, customValues = persistentListOf())
|
||||
|
||||
// Act
|
||||
val actual = converter(normalFee = bitcoinFee, shouldDisableCustomFee = false)
|
||||
.convert(FeeItemConverter.Input(TransactionFee.Single(bitcoinFee), customFee = provided))
|
||||
|
||||
// Assert
|
||||
assertThat(actual.last()).isEqualTo(provided)
|
||||
}
|
||||
}
|
||||
|
||||
private fun none() = FeeSelectorParams.FeeStateConfiguration.None
|
||||
private fun excludeLow() = FeeSelectorParams.FeeStateConfiguration.ExcludeLow
|
||||
private fun suggestion() = FeeSelectorParams.FeeStateConfiguration.Suggestion(title = mockk(), fee = commonFee())
|
||||
|
||||
private fun ethFee(value: String) =
|
||||
Fee.Common(Amount(currencySymbol = "ETH", value = BigDecimal(value), decimals = 18))
|
||||
|
||||
data class ItemsModel(
|
||||
val config: FeeSelectorParams.FeeStateConfiguration,
|
||||
val transactionFee: TransactionFee,
|
||||
val expectedTypes: List<Class<out FeeItem>>,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package com.tangem.features.send.feeselector.model.transformers
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.features.send.loadedStatus
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class FeeSelectorCustomFieldConverterTest {
|
||||
|
||||
private val currencyFactory = MockCryptoCurrencyFactory()
|
||||
|
||||
// Bitcoin network so the Bitcoin converter passes its isUseBitcoinFeeConverter() check; other converters
|
||||
// don't read the network, so a single status drives every dispatch branch.
|
||||
private val feeStatus = loadedStatus(
|
||||
currency = currencyFactory.createCoin(Blockchain.Bitcoin),
|
||||
fiatRate = BigDecimal("50000"),
|
||||
)
|
||||
|
||||
private val commonFee: Fee = Fee.Common(Amount(Blockchain.Ethereum))
|
||||
private val bitcoinFee: Fee = Fee.Bitcoin(
|
||||
amount = Amount(currencySymbol = "BTC", value = BigDecimal("0.0001"), decimals = 8),
|
||||
satoshiPerByte = BigDecimal("10"),
|
||||
txSize = BigDecimal("250"),
|
||||
)
|
||||
private val ethereumFee: Fee = Fee.Ethereum.EIP1559(
|
||||
amount = Amount(Blockchain.Ethereum),
|
||||
gasLimit = BigInteger.valueOf(21_000),
|
||||
maxFeePerGas = BigInteger.valueOf(30_000_000_000),
|
||||
priorityFee = BigInteger.valueOf(2_000_000_000),
|
||||
)
|
||||
private val kaspaFee: Fee = Fee.Kaspa(
|
||||
amount = Amount(currencySymbol = "KAS", value = BigDecimal("0.0001"), decimals = 8),
|
||||
mass = BigInteger.valueOf(2000),
|
||||
feeRate = BigInteger.valueOf(5),
|
||||
)
|
||||
|
||||
private fun converter(normalFee: Fee = commonFee) = FeeSelectorCustomFieldConverter(
|
||||
feeSelectorIntents = mockk(relaxed = true),
|
||||
appCurrency = AppCurrency.Default,
|
||||
feeCryptoCurrencyStatus = feeStatus,
|
||||
normalFee = normalFee,
|
||||
)
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `GIVEN fee type WHEN convert THEN routed to matching custom fee converter`(model: DispatchModel) {
|
||||
// Act
|
||||
val actual = converter().convert(model.fee)
|
||||
|
||||
// Assert (each converter emits a distinct number of fields - a fingerprint of correct routing)
|
||||
assertThat(actual).hasSize(model.expectedFieldCount)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
DispatchModel(fee = bitcoinFee, expectedFieldCount = 2), // amount + satoshi/byte
|
||||
DispatchModel(fee = ethereumFee, expectedFieldCount = 4), // amount + maxFee + priority + gasLimit
|
||||
DispatchModel(fee = kaspaFee, expectedFieldCount = 1), // amount
|
||||
DispatchModel(fee = commonFee, expectedFieldCount = 0), // unsupported -> empty
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty custom values WHEN convertBack THEN returns normal fee unchanged`() {
|
||||
// Act
|
||||
val actual = converter(normalFee = commonFee).convertBack(persistentListOf())
|
||||
|
||||
// Assert
|
||||
assertThat(actual).isSameInstanceAs(commonFee)
|
||||
}
|
||||
|
||||
data class DispatchModel(val fee: Fee, val expectedFieldCount: Int)
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package com.tangem.features.send.feeselector.model.transformers
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.loadedStatus
|
||||
import com.tangem.features.send.subcomponents.fee.model.converters.custom.kaspa.KaspaCustomFeeConverter
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class FeeSelectorCustomValueChangedTransformerTest {
|
||||
|
||||
private val currencyFactory = MockCryptoCurrencyFactory()
|
||||
|
||||
private val feeStatus = loadedStatus(
|
||||
currency = currencyFactory.createCoin(Blockchain.Kaspa),
|
||||
fiatRate = BigDecimal("0.1"),
|
||||
)
|
||||
|
||||
private val kaspaFee = Fee.Kaspa(
|
||||
amount = Amount(currencySymbol = "KAS", value = BigDecimal("0.0001"), decimals = 8),
|
||||
mass = BigInteger.valueOf(2000),
|
||||
feeRate = BigInteger.valueOf(5),
|
||||
)
|
||||
|
||||
private val customItem = FeeItem.Custom(
|
||||
fee = kaspaFee,
|
||||
customValues = KaspaCustomFeeConverter(
|
||||
onCustomFeeValueChange = { _, _ -> },
|
||||
appCurrency = AppCurrency.Default,
|
||||
feeCryptoCurrencyStatus = feeStatus,
|
||||
).convert(kaspaFee),
|
||||
)
|
||||
|
||||
private fun transformer(index: Int, value: String) = FeeSelectorCustomValueChangedTransformer(
|
||||
index = index,
|
||||
value = value,
|
||||
intents = mockk(relaxed = true),
|
||||
appCurrency = AppCurrency.Default,
|
||||
feeCryptoCurrencyStatus = feeStatus,
|
||||
)
|
||||
|
||||
private fun content(feeItems: List<FeeItem>, selected: FeeItem) = FeeSelectorUM.Content(
|
||||
isPrimaryButtonEnabled = true,
|
||||
fees = TransactionFee.Single(normal = kaspaFee),
|
||||
feeItems = feeItems.toImmutableList(),
|
||||
selectedFeeItem = selected,
|
||||
feeExtraInfo = mockk(),
|
||||
feeFiatRateUM = null,
|
||||
feeNonce = FeeNonce.None,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN custom fee and non-zero value WHEN transform THEN custom updated selected and button enabled`() {
|
||||
// Arrange
|
||||
val state = content(feeItems = listOf(customItem), selected = customItem)
|
||||
|
||||
// Act (index 0 = amount field of the Kaspa custom fee)
|
||||
val result = transformer(index = 0, value = "0.0002").transform(state) as FeeSelectorUM.Content
|
||||
|
||||
// Assert
|
||||
assertThat(result.isPrimaryButtonEnabled).isTrue()
|
||||
assertThat(result.selectedFeeItem).isInstanceOf(FeeItem.Custom::class.java)
|
||||
val updatedCustom = result.feeItems.filterIsInstance<FeeItem.Custom>().first()
|
||||
assertThat(updatedCustom.customValues.first().value).isEqualTo("0.0002")
|
||||
assertThat(result.selectedFeeItem).isEqualTo(updatedCustom)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN custom fee edited to zero WHEN transform THEN button disabled`() {
|
||||
// Arrange
|
||||
val state = content(feeItems = listOf(customItem), selected = customItem)
|
||||
|
||||
// Act
|
||||
val result = transformer(index = 0, value = "0").transform(state) as FeeSelectorUM.Content
|
||||
|
||||
// Assert
|
||||
assertThat(result.isPrimaryButtonEnabled).isFalse()
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `GIVEN non-applicable state WHEN transform THEN returned unchanged`(model: UnchangedModel) {
|
||||
// Act
|
||||
val result = transformer(index = 0, value = "0.0002").transform(model.state)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isSameInstanceAs(model.state)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
UnchangedModel(state = FeeSelectorUM.Loading), // not a content state
|
||||
UnchangedModel(state = content(feeItems = listOf(FeeItem.Market(kaspaFee)), selected = FeeItem.Market(kaspaFee))),
|
||||
)
|
||||
|
||||
data class UnchangedModel(val state: FeeSelectorUM)
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.features.send.feeselector.model.transformers
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.commonFee
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class FeeSelectorErrorTransformerTest {
|
||||
|
||||
private val fee = commonFee()
|
||||
|
||||
private fun content() = FeeSelectorUM.Content(
|
||||
isPrimaryButtonEnabled = true,
|
||||
fees = TransactionFee.Single(normal = fee),
|
||||
feeItems = persistentListOf(FeeItem.Market(fee)),
|
||||
selectedFeeItem = FeeItem.Market(fee),
|
||||
feeExtraInfo = FeeExtraInfo(
|
||||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
feeCryptoCurrencyStatus = mockk(),
|
||||
),
|
||||
feeFiatRateUM = null,
|
||||
feeNonce = FeeNonce.None,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN content state and not-enough-funds error WHEN transform THEN stays content with flag and disabled button`() {
|
||||
// Act
|
||||
val result = FeeSelectorErrorTransformer(GetFeeError.GaslessError.NotEnoughFunds)
|
||||
.transform(content()) as FeeSelectorUM.Content
|
||||
|
||||
// Assert
|
||||
assertThat(result.isPrimaryButtonEnabled).isFalse()
|
||||
assertThat(result.feeExtraInfo.isNotEnoughFunds).isTrue()
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `GIVEN other state or error WHEN transform THEN transitions to error`(model: ErrorModel) {
|
||||
// Act
|
||||
val result = FeeSelectorErrorTransformer(model.error).transform(model.state)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(FeeSelectorUM.Error(error = model.error))
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
// content but a different error -> the special branch needs NotEnoughFunds specifically
|
||||
ErrorModel(state = content(), error = GetFeeError.UnknownError),
|
||||
// not-enough-funds but not a content state -> the special branch needs a Content state
|
||||
ErrorModel(state = FeeSelectorUM.Loading, error = GetFeeError.GaslessError.NotEnoughFunds),
|
||||
)
|
||||
|
||||
data class ErrorModel(val state: FeeSelectorUM, val error: GetFeeError)
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
package com.tangem.features.send.feeselector.model.transformers
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams
|
||||
import com.tangem.features.send.feeselector.model.FeeSelectorLogic
|
||||
import com.tangem.features.send.loadedStatus
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class FeeSelectorLoadedTransformerTest {
|
||||
|
||||
private val currencyFactory = MockCryptoCurrencyFactory()
|
||||
private val coin: CryptoCurrency = currencyFactory.ethereum
|
||||
|
||||
private val commonFee: Fee = Fee.Common(Amount(Blockchain.Ethereum))
|
||||
private val ethereumFee: Fee = Fee.Ethereum.Legacy(
|
||||
amount = Amount(Blockchain.Ethereum),
|
||||
gasLimit = BigInteger.valueOf(21_000),
|
||||
gasPrice = BigInteger.valueOf(1_000_000_000),
|
||||
)
|
||||
|
||||
private fun status(currency: CryptoCurrency = coin): CryptoCurrencyStatus =
|
||||
loadedStatus(currency = currency, fiatRate = BigDecimal("2000"))
|
||||
|
||||
private fun basic(normal: Fee): FeeSelectorLogic.LoadedFeeResult =
|
||||
FeeSelectorLogic.LoadedFeeResult.Basic(TransactionFee.Choosable(normal = normal, minimum = normal, priority = normal))
|
||||
|
||||
private fun transformer(
|
||||
fees: FeeSelectorLogic.LoadedFeeResult,
|
||||
feeStateConfiguration: FeeSelectorParams.FeeStateConfiguration = FeeSelectorParams.FeeStateConfiguration.None,
|
||||
) = FeeSelectorLoadedTransformer(
|
||||
cryptoCurrencyStatus = status(),
|
||||
feeCryptoCurrencyStatus = status(),
|
||||
appCurrency = AppCurrency.Default,
|
||||
fees = fees,
|
||||
feeStateConfiguration = feeStateConfiguration,
|
||||
isFeeApproximate = false,
|
||||
feeSelectorIntents = mockk(relaxed = true),
|
||||
shouldDisableCustomFee = true,
|
||||
)
|
||||
|
||||
private fun prevContent(selected: FeeItem, feeNonce: FeeNonce = FeeNonce.None) = FeeSelectorUM.Content(
|
||||
isPrimaryButtonEnabled = true,
|
||||
fees = TransactionFee.Single(normal = commonFee),
|
||||
feeItems = persistentListOf(selected),
|
||||
selectedFeeItem = selected,
|
||||
feeExtraInfo = mockk(),
|
||||
feeFiatRateUM = null,
|
||||
feeNonce = feeNonce,
|
||||
)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class SelectedFee {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `GIVEN previous state WHEN transform THEN selected fee item resolved`(model: SelectedModel) {
|
||||
// Act
|
||||
val result = transformer(basic(commonFee)).transform(model.prevState) as FeeSelectorUM.Content
|
||||
|
||||
// Assert
|
||||
assertThat(result.selectedFeeItem).isInstanceOf(model.expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
// no prior selection -> defaults to market (no suggested in this config)
|
||||
SelectedModel(FeeSelectorUM.Loading, FeeItem.Market::class.java),
|
||||
// prior loading selection -> market
|
||||
SelectedModel(prevContent(FeeItem.Loading), FeeItem.Market::class.java),
|
||||
// prior concrete selection -> same class preserved
|
||||
SelectedModel(prevContent(FeeItem.Fast(commonFee)), FeeItem.Fast::class.java),
|
||||
// prior class no longer present -> falls back to loading
|
||||
SelectedModel(prevContent(FeeItem.Suggested(title = mockk(), fee = commonFee)), FeeItem.Loading::class.java),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN selection falls back to loading WHEN transform THEN primary button disabled`() {
|
||||
// Act
|
||||
val result = transformer(basic(commonFee))
|
||||
.transform(prevContent(FeeItem.Suggested(title = mockk(), fee = commonFee))) as FeeSelectorUM.Content
|
||||
|
||||
// Assert
|
||||
assertThat(result.selectedFeeItem).isEqualTo(FeeItem.Loading)
|
||||
assertThat(result.isPrimaryButtonEnabled).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN resolved fee item WHEN transform THEN primary button enabled`() {
|
||||
// Act
|
||||
val result = transformer(basic(commonFee)).transform(FeeSelectorUM.Loading) as FeeSelectorUM.Content
|
||||
|
||||
// Assert
|
||||
assertThat(result.isPrimaryButtonEnabled).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no prior selection and suggested available WHEN transform THEN suggested preselected`() {
|
||||
// Arrange (Suggestion config makes the converter emit a Suggested item)
|
||||
val config = FeeSelectorParams.FeeStateConfiguration.Suggestion(title = mockk(), fee = commonFee)
|
||||
|
||||
// Act
|
||||
val result = transformer(basic(commonFee), feeStateConfiguration = config)
|
||||
.transform(FeeSelectorUM.Loading) as FeeSelectorUM.Content
|
||||
|
||||
// Assert
|
||||
assertThat(result.selectedFeeItem).isInstanceOf(FeeItem.Suggested::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Nonce {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `GIVEN normal fee type WHEN transform THEN nonce field present only for ethereum`(model: NonceTypeModel) {
|
||||
// Act
|
||||
val result = transformer(basic(model.normal)).transform(FeeSelectorUM.Loading) as FeeSelectorUM.Content
|
||||
|
||||
// Assert
|
||||
assertThat(result.feeNonce).isInstanceOf(model.expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
NonceTypeModel(ethereumFee, FeeNonce.Nonce::class.java),
|
||||
NonceTypeModel(commonFee, FeeNonce.None::class.java),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN ethereum fee and previous nonce WHEN transform THEN previous nonce preserved`() {
|
||||
// Arrange
|
||||
val prev = prevContent(
|
||||
selected = FeeItem.Market(commonFee),
|
||||
feeNonce = FeeNonce.Nonce(nonce = BigInteger.valueOf(7), onNonceChange = {}),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = transformer(basic(ethereumFee)).transform(prev) as FeeSelectorUM.Content
|
||||
|
||||
// Assert
|
||||
assertThat((result.feeNonce as FeeNonce.Nonce).nonce).isEqualTo(BigInteger.valueOf(7))
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class ExtraInfo {
|
||||
|
||||
@Test
|
||||
fun `GIVEN basic fee result WHEN transform THEN extra info reflects basic non-tron status`() {
|
||||
// Act
|
||||
val result = transformer(basic(commonFee)).transform(FeeSelectorUM.Loading) as FeeSelectorUM.Content
|
||||
|
||||
// Assert
|
||||
val info = result.feeExtraInfo
|
||||
assertThat(info.availableFeeCurrencies).isNull() // Extended-only
|
||||
assertThat(info.transactionFeeExtended).isNull() // Extended-only
|
||||
assertThat(info.isTronToken).isFalse()
|
||||
assertThat(info.isFeeConvertibleToFiat).isEqualTo(coin.network.hasFiatFeeRate)
|
||||
assertThat(result.feeFiatRateUM).isNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
data class SelectedModel(val prevState: FeeSelectorUM, val expected: Class<out FeeItem>)
|
||||
data class NonceTypeModel(val normal: Fee, val expected: Class<out FeeNonce>)
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package com.tangem.features.send.feeselector.model.transformers
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.commonFee
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import java.math.BigInteger
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class FeeSelectorNonceChangeTransformerTest {
|
||||
|
||||
private val fee = commonFee()
|
||||
|
||||
private fun content(feeNonce: FeeNonce) = FeeSelectorUM.Content(
|
||||
isPrimaryButtonEnabled = true,
|
||||
fees = TransactionFee.Single(normal = fee),
|
||||
feeItems = persistentListOf(FeeItem.Market(fee)),
|
||||
selectedFeeItem = FeeItem.Market(fee),
|
||||
feeExtraInfo = mockk<FeeExtraInfo>(),
|
||||
feeFiatRateUM = null,
|
||||
feeNonce = feeNonce,
|
||||
)
|
||||
|
||||
private fun nonceState(nonce: BigInteger?) = content(FeeNonce.Nonce(nonce = nonce, onNonceChange = {}))
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Update {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `GIVEN nonce field WHEN transform THEN nonce updated`(model: UpdateModel) {
|
||||
// Arrange
|
||||
val state = nonceState(nonce = BigInteger.ONE)
|
||||
|
||||
// Act
|
||||
val result = FeeSelectorNonceChangeTransformer(model.value).transform(state) as FeeSelectorUM.Content
|
||||
|
||||
// Assert
|
||||
assertThat((result.feeNonce as FeeNonce.Nonce).nonce).isEqualTo(model.expectedNonce)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
UpdateModel(value = "42", expectedNonce = BigInteger.valueOf(42)), // valid number
|
||||
UpdateModel(value = "", expectedNonce = null), // empty -> cleared
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Unchanged {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `GIVEN non-applicable input WHEN transform THEN state returned unchanged`(model: UnchangedModel) {
|
||||
// Act
|
||||
val result = FeeSelectorNonceChangeTransformer(model.value).transform(model.state)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isSameInstanceAs(model.state)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
UnchangedModel(value = "abc", state = nonceState(nonce = BigInteger.ONE)), // non-numeric
|
||||
UnchangedModel(value = "42", state = content(FeeNonce.None)), // no editable nonce
|
||||
UnchangedModel(value = "42", state = FeeSelectorUM.Loading), // not a content state
|
||||
)
|
||||
}
|
||||
|
||||
data class UpdateModel(val value: String, val expectedNonce: BigInteger?)
|
||||
data class UnchangedModel(val value: String, val state: FeeSelectorUM)
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package com.tangem.features.send.feeselector.model.transformers
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.commonFee
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class FeeSelectorRemoveSuggestedTransformerTest {
|
||||
|
||||
private val fee = commonFee()
|
||||
private val market = FeeItem.Market(fee)
|
||||
private val fast = FeeItem.Fast(fee)
|
||||
private val suggested = FeeItem.Suggested(title = TextReference.EMPTY, fee = fee)
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `GIVEN suggested present WHEN transform THEN suggested removed and selection resolved`(model: SelectionModel) {
|
||||
// Arrange
|
||||
val state = content(feeItems = listOf(suggested, market, fast), selected = model.selected)
|
||||
|
||||
// Act
|
||||
val result = FeeSelectorRemoveSuggestedTransformer.transform(state) as FeeSelectorUM.Content
|
||||
|
||||
// Assert
|
||||
assertThat(result.feeItems).containsExactly(market, fast).inOrder()
|
||||
assertThat(result.selectedFeeItem).isEqualTo(model.expectedSelected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
SelectionModel(selected = suggested, expectedSelected = market),
|
||||
SelectionModel(selected = fast, expectedSelected = fast),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-content state WHEN transform THEN returned unchanged`() {
|
||||
// Act
|
||||
val result = FeeSelectorRemoveSuggestedTransformer.transform(FeeSelectorUM.Loading)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(FeeSelectorUM.Loading)
|
||||
}
|
||||
|
||||
private fun content(feeItems: List<FeeItem>, selected: FeeItem) = FeeSelectorUM.Content(
|
||||
isPrimaryButtonEnabled = true,
|
||||
fees = TransactionFee.Single(normal = fee),
|
||||
feeItems = feeItems.toImmutableList(),
|
||||
selectedFeeItem = selected,
|
||||
feeExtraInfo = mockk<FeeExtraInfo>(),
|
||||
feeFiatRateUM = null,
|
||||
feeNonce = FeeNonce.None,
|
||||
)
|
||||
|
||||
data class SelectionModel(val selected: FeeItem, val expectedSelected: FeeItem)
|
||||
}
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
package com.tangem.features.send.send
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.right
|
||||
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
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
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.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase
|
||||
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
|
||||
import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase
|
||||
import com.tangem.domain.settings.NeverShowTapHelpUseCase
|
||||
import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.quotes.IsHighNetworkFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForGaslessUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.send.api.SendComponent
|
||||
import com.tangem.features.send.api.SendFeatureToggles
|
||||
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.api.entity.PredefinedValues
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountReduceTrigger
|
||||
import com.tangem.features.send.api.subcomponents.amount.SendAmountUpdateTrigger
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorCheckReloadListener
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorCheckReloadTrigger
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateListener
|
||||
import com.tangem.features.send.api.subcomponents.notifications.SendNotificationsUpdateTrigger
|
||||
import com.tangem.features.send.common.SendBalanceUpdater
|
||||
import com.tangem.features.send.common.SendConfirmAlertFactory
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.send.analytics.SendAnalyticHelper
|
||||
import com.tangem.features.send.send.confirm.SendConfirmComponent
|
||||
import com.tangem.features.send.send.confirm.model.SendConfirmModel
|
||||
import com.tangem.features.send.send.model.SendModel
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
import com.tangem.features.send.testDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal abstract class SendModelTestBase {
|
||||
|
||||
protected val testUserWalletId = UserWalletId("1234567890ABCDEF")
|
||||
protected val testCryptoCurrency: CryptoCurrency = mockk(relaxed = true)
|
||||
protected val testUserWallet: UserWallet = mockk(relaxed = true)
|
||||
protected val testCryptoCurrencyStatus: CryptoCurrencyStatus = mockk(relaxed = true) {
|
||||
io.mockk.every { currency } returns testCryptoCurrency
|
||||
}
|
||||
|
||||
protected val router: Router = mockk(relaxed = true)
|
||||
protected val appRouter: AppRouter = mockk(relaxed = true)
|
||||
protected val getUserWalletUseCase: GetUserWalletUseCase = mockk(relaxed = true)
|
||||
protected val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase =
|
||||
mockk(relaxed = true)
|
||||
protected val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase = mockk(relaxed = true)
|
||||
protected val listenToQrScanningUseCase: ListenToQrScanningUseCase = mockk(relaxed = true)
|
||||
protected val parseQrCodeUseCase: ParseQrCodeUseCase = mockk(relaxed = true)
|
||||
protected val sendConfirmAlertFactory: SendConfirmAlertFactory = mockk(relaxed = true)
|
||||
protected val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase = mockk(relaxed = true)
|
||||
protected val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase = mockk(relaxed = true)
|
||||
protected val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase = mockk(relaxed = true)
|
||||
protected val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase = mockk(relaxed = true)
|
||||
protected val createTransferTransactionUseCase: CreateTransferTransactionUseCase = mockk(relaxed = true)
|
||||
protected val getFeeUseCase: GetFeeUseCase = mockk(relaxed = true)
|
||||
protected val getFeeForGaslessUseCase: GetFeeForGaslessUseCase = mockk(relaxed = true)
|
||||
protected val getFeeForTokenUseCase: GetFeeForTokenUseCase = mockk(relaxed = true)
|
||||
protected val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase = mockk(relaxed = true)
|
||||
protected val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase = mockk(relaxed = true)
|
||||
protected val sendAmountUpdateTrigger: SendAmountUpdateTrigger = mockk(relaxed = true)
|
||||
protected val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
protected val sendTransactionUseCase: SendTransactionUseCase = mockk(relaxed = true)
|
||||
|
||||
// SendConfirmModel-specific dependencies
|
||||
protected val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase = mockk(relaxed = true)
|
||||
protected val neverShowTapHelpUseCase: NeverShowTapHelpUseCase = mockk(relaxed = true)
|
||||
protected val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase = mockk(relaxed = true)
|
||||
protected val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase = mockk(relaxed = true)
|
||||
protected val feeSelectorCheckReloadListener: FeeSelectorCheckReloadListener = mockk(relaxed = true)
|
||||
protected val feeSelectorCheckReloadTrigger: FeeSelectorCheckReloadTrigger = mockk(relaxed = true)
|
||||
protected val notificationsUpdateTrigger: SendNotificationsUpdateTrigger = mockk(relaxed = true)
|
||||
protected val notificationsUpdateListener: SendNotificationsUpdateListener = mockk(relaxed = true)
|
||||
protected val urlOpener: UrlOpener = mockk(relaxed = true)
|
||||
protected val shareManager: ShareManager = mockk(relaxed = true)
|
||||
protected val feeSelectorReloadTrigger: FeeSelectorReloadTrigger = mockk(relaxed = true)
|
||||
protected val sendAmountReduceTrigger: SendAmountReduceTrigger = mockk(relaxed = true)
|
||||
protected val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase = mockk(relaxed = true)
|
||||
protected val currenciesRepository: CurrenciesRepository = mockk(relaxed = true)
|
||||
protected val createAndSendGaslessTransactionUseCase: CreateAndSendGaslessTransactionUseCase = mockk(relaxed = true)
|
||||
protected val isHighNetworkFeeUseCase: IsHighNetworkFeeUseCase = mockk(relaxed = true)
|
||||
protected val sendFeatureToggles: SendFeatureToggles = mockk(relaxed = true)
|
||||
protected val sendAnalyticHelper: SendAnalyticHelper = mockk(relaxed = true)
|
||||
protected val sendBalanceUpdaterFactory: SendBalanceUpdater.Factory = mockk(relaxed = true)
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
MockKAnnotations.init(this)
|
||||
|
||||
// Reset recorded calls on use-cases asserted via coVerify(exactly=N). PER_CLASS parameterized
|
||||
// tests (e.g. SendConfirmModelTest) reuse one instance, so calls would otherwise accumulate
|
||||
// across rows. answers=false keeps the happy-path stubs re-applied below.
|
||||
clearMocks(
|
||||
createTransferTransactionUseCase,
|
||||
sendTransactionUseCase,
|
||||
createAndSendGaslessTransactionUseCase,
|
||||
feeSelectorCheckReloadTrigger,
|
||||
answers = false,
|
||||
recordedCalls = true,
|
||||
childMocks = false,
|
||||
)
|
||||
|
||||
// --- SendModel init-path happy stubs ---
|
||||
every { getUserWalletUseCase(testUserWalletId) } returns testUserWallet.right()
|
||||
coEvery { getSelectedAppCurrencyUseCase.invokeSync() } returns AppCurrency.Default.right()
|
||||
every { getSelectedAppCurrencyUseCase() } returns flowOf(AppCurrency.Default.right())
|
||||
every { listenToQrScanningUseCase(SourceType.SEND) } returns emptyFlow<String>().right()
|
||||
every { getBalanceHidingSettingsUseCase() } returns emptyFlow()
|
||||
every { getAccountCurrencyStatusUseCase(testUserWalletId, testCryptoCurrency) } returns emptyFlow()
|
||||
coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns false
|
||||
coEvery { getFeePaidCryptoCurrencyStatusSyncUseCase(any(), any()) } returns testCryptoCurrencyStatus.right()
|
||||
// no-fee overload (disambiguated by memo: String at position 2); 6 matchers cover defaulted nonce
|
||||
coEvery {
|
||||
createTransferTransactionUseCase(any(), any<String>(), any(), any(), any(), any())
|
||||
} returns mockk<TransactionData.Uncompiled>(relaxed = true).right()
|
||||
// with-fee overload (disambiguated by Fee at position 2); 7 matchers cover defaulted nonce
|
||||
coEvery {
|
||||
createTransferTransactionUseCase(any(), any<Fee>(), any(), any(), any(), any(), any())
|
||||
} returns mockk<TransactionData.Uncompiled>(relaxed = true).right()
|
||||
coEvery { sendTransactionUseCase(any(), any(), any()) } returns "txHash".right()
|
||||
coEvery { createAndSendGaslessTransactionUseCase(any(), any(), any()) } returns "txHash".right()
|
||||
every { getExplorerTransactionUrlUseCase(any(), any()) } returns "https://explorer/tx".right()
|
||||
|
||||
// --- SendConfirmModel init-path happy stubs ---
|
||||
coEvery { isSendTapHelpEnabledUseCase.invokeSync() } returns false.right()
|
||||
every { isSendTapHelpEnabledUseCase() } returns emptyFlow<Boolean>().right()
|
||||
coEvery { isAmountSubtractAvailableUseCase(any(), any(), any()) } returns false.right()
|
||||
every { feeSelectorCheckReloadListener.checkReloadResultFlow } returns emptyFlow()
|
||||
every { notificationsUpdateListener.hasErrorFlow } returns emptyFlow()
|
||||
}
|
||||
|
||||
protected fun createSendModel(
|
||||
testScope: TestScope,
|
||||
paramsContainer: ParamsContainer = MutableParamsContainer(defaultSendParams()),
|
||||
): SendModel {
|
||||
return SendModel(
|
||||
paramsContainer = paramsContainer,
|
||||
dispatchers = testScope.testDispatcherProvider(),
|
||||
router = router,
|
||||
getUserWalletUseCase = getUserWalletUseCase,
|
||||
getFeePaidCryptoCurrencyStatusSyncUseCase = getFeePaidCryptoCurrencyStatusSyncUseCase,
|
||||
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
|
||||
listenToQrScanningUseCase = listenToQrScanningUseCase,
|
||||
parseQrCodeUseCase = parseQrCodeUseCase,
|
||||
sendConfirmAlertFactory = sendConfirmAlertFactory,
|
||||
saveBlockchainErrorUseCase = saveBlockchainErrorUseCase,
|
||||
getWalletMetaInfoUseCase = getWalletMetaInfoUseCase,
|
||||
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
|
||||
getBalanceHidingSettingsUseCase = getBalanceHidingSettingsUseCase,
|
||||
createTransferTransactionUseCase = createTransferTransactionUseCase,
|
||||
getFeeUseCase = getFeeUseCase,
|
||||
getFeeForGaslessUseCase = getFeeForGaslessUseCase,
|
||||
getFeeForTokenUseCase = getFeeForTokenUseCase,
|
||||
getAccountCurrencyStatusUseCase = getAccountCurrencyStatusUseCase,
|
||||
isAccountsModeEnabledUseCase = isAccountsModeEnabledUseCase,
|
||||
sendAmountUpdateTrigger = sendAmountUpdateTrigger,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
)
|
||||
}
|
||||
|
||||
protected fun createSendConfirmModel(
|
||||
testScope: TestScope,
|
||||
paramsContainer: ParamsContainer = MutableParamsContainer(defaultSendConfirmParams()),
|
||||
): SendConfirmModel {
|
||||
return SendConfirmModel(
|
||||
paramsContainer = paramsContainer,
|
||||
dispatchers = testScope.testDispatcherProvider(),
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
appRouter = appRouter,
|
||||
router = router,
|
||||
isSendTapHelpEnabledUseCase = isSendTapHelpEnabledUseCase,
|
||||
neverShowTapHelpUseCase = neverShowTapHelpUseCase,
|
||||
createTransferTransactionUseCase = createTransferTransactionUseCase,
|
||||
sendTransactionUseCase = sendTransactionUseCase,
|
||||
saveBlockchainErrorUseCase = saveBlockchainErrorUseCase,
|
||||
getWalletMetaInfoUseCase = getWalletMetaInfoUseCase,
|
||||
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
|
||||
getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase,
|
||||
isAmountSubtractAvailableUseCase = isAmountSubtractAvailableUseCase,
|
||||
feeSelectorCheckReloadListener = feeSelectorCheckReloadListener,
|
||||
feeSelectorCheckReloadTrigger = feeSelectorCheckReloadTrigger,
|
||||
notificationsUpdateTrigger = notificationsUpdateTrigger,
|
||||
notificationsUpdateListener = notificationsUpdateListener,
|
||||
alertFactory = sendConfirmAlertFactory,
|
||||
sendAnalyticHelper = sendAnalyticHelper,
|
||||
urlOpener = urlOpener,
|
||||
shareManager = shareManager,
|
||||
feeSelectorReloadTrigger = feeSelectorReloadTrigger,
|
||||
sendAmountReduceTrigger = sendAmountReduceTrigger,
|
||||
getBalanceHidingSettingsUseCase = getBalanceHidingSettingsUseCase,
|
||||
manageCryptoCurrenciesUseCase = manageCryptoCurrenciesUseCase,
|
||||
currenciesRepository = currenciesRepository,
|
||||
createAndSendGaslessTransactionUseCase = createAndSendGaslessTransactionUseCase,
|
||||
isHighNetworkFeeUseCase = isHighNetworkFeeUseCase,
|
||||
sendFeatureToggles = sendFeatureToggles,
|
||||
sendBalanceUpdaterFactory = sendBalanceUpdaterFactory,
|
||||
)
|
||||
}
|
||||
|
||||
protected open fun defaultSendParams(): SendComponent.Params = SendComponent.Params(
|
||||
userWalletId = testUserWalletId,
|
||||
currency = testCryptoCurrency,
|
||||
amount = null,
|
||||
destinationAddress = null,
|
||||
tag = null,
|
||||
transactionId = null,
|
||||
entryType = SendComponent.EntryType.Manual,
|
||||
callback = mockk(relaxed = true),
|
||||
)
|
||||
|
||||
protected fun defaultSendConfirmParams(
|
||||
state: SendUM = SendUM(
|
||||
amountUM = AmountState.Empty,
|
||||
destinationUM = DestinationUM.Empty(),
|
||||
feeSelectorUM = FeeSelectorUM.Loading,
|
||||
confirmUM = ConfirmUM.Empty,
|
||||
confirmData = null,
|
||||
),
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus = testCryptoCurrencyStatus,
|
||||
feeCryptoCurrencyStatus: CryptoCurrencyStatus = testCryptoCurrencyStatus,
|
||||
): SendConfirmComponent.Params = SendConfirmComponent.Params(
|
||||
state = state,
|
||||
analyticsCategoryName = "test_send",
|
||||
analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.Send,
|
||||
userWallet = testUserWallet,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
cryptoCurrencyStatusFlow = kotlinx.coroutines.flow.MutableStateFlow(cryptoCurrencyStatus),
|
||||
feeCryptoCurrencyStatusFlow = kotlinx.coroutines.flow.MutableStateFlow(feeCryptoCurrencyStatus),
|
||||
accountFlow = kotlinx.coroutines.flow.MutableStateFlow(null),
|
||||
isAccountModeFlow = kotlinx.coroutines.flow.MutableStateFlow(false),
|
||||
appCurrency = AppCurrency.Default,
|
||||
callback = mockk(relaxed = true),
|
||||
isBalanceHidingFlow = kotlinx.coroutines.flow.MutableStateFlow(false),
|
||||
predefinedValues = PredefinedValues.Empty,
|
||||
onLoadFee = { Either.Right(mockk(relaxed = true)) },
|
||||
onLoadFeeExtended = { Either.Right(mockk(relaxed = true)) },
|
||||
onSendTransaction = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,317 @@
|
|||
package com.tangem.features.send.send.confirm.model
|
||||
|
||||
import android.os.SystemClock
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.send.SendModelTestBase
|
||||
import com.tangem.features.send.send.ui.state.SendUM
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import java.math.BigDecimal
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class SendConfirmModelTest : SendModelTestBase() {
|
||||
|
||||
@BeforeEach
|
||||
fun mockSystemClock() {
|
||||
// SystemClock.elapsedRealtime() is read in init/subscription paths; default to a fresh timer.
|
||||
mockkStatic(SystemClock::class)
|
||||
every { SystemClock.elapsedRealtime() } returns 0L
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(SystemClock::class)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class OnSendClick {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `WHEN onSendClick THEN send fresh fee else trigger check reload`(model: OnSendClickModel) = runTest {
|
||||
// Arrange
|
||||
every { SystemClock.elapsedRealtime() } returns model.elapsedRealtime
|
||||
val sut = createSendConfirmModel(this, confirmParams(normalFeeState()))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
sut.onSendClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
if (model.expectedSendInitiated) {
|
||||
coVerify(exactly = 1) {
|
||||
createTransferTransactionUseCase(
|
||||
any(),
|
||||
any<Fee>(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any()
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 0) { feeSelectorCheckReloadTrigger.triggerCheckUpdate() }
|
||||
} else {
|
||||
coVerify(exactly = 0) {
|
||||
createTransferTransactionUseCase(
|
||||
any(),
|
||||
any<Fee>(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any()
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 1) { feeSelectorCheckReloadTrigger.triggerCheckUpdate() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
// diff = elapsedRealtime - sendIdleTimer(0); < 10s = fresh -> verify & send
|
||||
OnSendClickModel(elapsedRealtime = 0L, expectedSendInitiated = true),
|
||||
OnSendClickModel(elapsedRealtime = 20_000L, expectedSendInitiated = false),
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class CheckFeeResult {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `WHEN check reload result emitted THEN send transaction only on success`(model: CheckFeeResultModel) =
|
||||
runTest {
|
||||
// Arrange
|
||||
val resultFlow = MutableSharedFlow<Boolean>(extraBufferCapacity = 1)
|
||||
every { feeSelectorCheckReloadListener.checkReloadResultFlow } returns resultFlow
|
||||
createSendConfirmModel(this, confirmParams(normalFeeState()))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
resultFlow.tryEmit(model.checkResult)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
if (model.expectedSendInitiated) {
|
||||
coVerify(exactly = 1) {
|
||||
createTransferTransactionUseCase(
|
||||
any(),
|
||||
any<Fee>(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
coVerify(exactly = 0) {
|
||||
createTransferTransactionUseCase(
|
||||
any(),
|
||||
any<Fee>(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
CheckFeeResultModel(checkResult = true, expectedSendInitiated = true),
|
||||
CheckFeeResultModel(checkResult = false, expectedSendInitiated = false),
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class SendTransactionDispatch {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `WHEN send THEN use gasless use case only for token-currency fee`(model: DispatchModel) = runTest {
|
||||
// Arrange
|
||||
val state = if (model.isTokenCurrencyFee) gaslessFeeState() else normalFeeState()
|
||||
val resultFlow = MutableSharedFlow<Boolean>(extraBufferCapacity = 1)
|
||||
every { feeSelectorCheckReloadListener.checkReloadResultFlow } returns resultFlow
|
||||
createSendConfirmModel(this, confirmParams(state))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
resultFlow.tryEmit(true)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
if (model.isTokenCurrencyFee) {
|
||||
coVerify(exactly = 1) { createAndSendGaslessTransactionUseCase(any(), any(), any()) }
|
||||
coVerify(exactly = 0) { sendTransactionUseCase(any(), any(), any()) }
|
||||
} else {
|
||||
coVerify(exactly = 0) { createAndSendGaslessTransactionUseCase(any(), any(), any()) }
|
||||
coVerify(exactly = 1) { sendTransactionUseCase(any(), any(), any()) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
DispatchModel(isTokenCurrencyFee = true),
|
||||
DispatchModel(isTokenCurrencyFee = false),
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class VerifyAndSend {
|
||||
|
||||
@Test
|
||||
fun `GIVEN successful send WHEN verifyAndSend THEN notify onSendTransaction`() = runTest {
|
||||
// Arrange
|
||||
val onSendTransaction = mockk<() -> Unit>(relaxed = true)
|
||||
val callback =
|
||||
mockk<com.tangem.features.send.send.confirm.SendConfirmComponent.ModelCallback>(relaxed = true)
|
||||
val resultFlow = MutableSharedFlow<Boolean>(extraBufferCapacity = 1)
|
||||
every { feeSelectorCheckReloadListener.checkReloadResultFlow } returns resultFlow
|
||||
coEvery { sendTransactionUseCase(any(), any(), any()) } returns "txHash".right()
|
||||
val params = MutableParamsContainer(
|
||||
defaultSendConfirmParams(
|
||||
state = normalFeeState(),
|
||||
cryptoCurrencyStatus = loadedFeeStatus,
|
||||
feeCryptoCurrencyStatus = loadedFeeStatus,
|
||||
).copy(onSendTransaction = onSendTransaction, callback = callback),
|
||||
)
|
||||
createSendConfirmModel(this, params)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
resultFlow.tryEmit(true)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) { onSendTransaction.invoke() }
|
||||
verify(exactly = 1) { callback.onResult(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN transaction creation fails WHEN verifyAndSend THEN show generic error and do NOT send`() = runTest {
|
||||
// Arrange
|
||||
val resultFlow = MutableSharedFlow<Boolean>(extraBufferCapacity = 1)
|
||||
every { feeSelectorCheckReloadListener.checkReloadResultFlow } returns resultFlow
|
||||
coEvery {
|
||||
createTransferTransactionUseCase(any(), any<Fee>(), any(), any(), any(), any(), any())
|
||||
} returns IllegalStateException("boom").left()
|
||||
createSendConfirmModel(this, confirmParams(normalFeeState()))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
resultFlow.tryEmit(true)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) { sendConfirmAlertFactory.getGenericErrorState(any(), any()) }
|
||||
coVerify(exactly = 0) { sendTransactionUseCase(any(), any(), any()) }
|
||||
}
|
||||
}
|
||||
|
||||
// region fixtures
|
||||
|
||||
private fun confirmParams(state: SendUM) = MutableParamsContainer(
|
||||
defaultSendConfirmParams(
|
||||
state = state,
|
||||
cryptoCurrencyStatus = loadedFeeStatus,
|
||||
feeCryptoCurrencyStatus = loadedFeeStatus,
|
||||
),
|
||||
)
|
||||
|
||||
/** Populated Content state with a regular (main-currency) fee — drives the normal send path. */
|
||||
private fun normalFeeState(): SendUM = contentState(
|
||||
fee = realFee(),
|
||||
transactionFeeExtended = null,
|
||||
)
|
||||
|
||||
/** Populated Content state where the extended fee is a gasless token-currency fee. */
|
||||
private fun gaslessFeeState(): SendUM = contentState(
|
||||
fee = realFee(),
|
||||
transactionFeeExtended = TransactionFeeExtended(
|
||||
transactionFee = TransactionFee.Single(normal = tokenFee()),
|
||||
feeTokenId = testCryptoCurrency.id,
|
||||
),
|
||||
)
|
||||
|
||||
private fun contentState(fee: Fee, transactionFeeExtended: TransactionFeeExtended?): SendUM {
|
||||
val amount = mockk<AmountState.Data>(relaxed = true) {
|
||||
every { amountTextField.cryptoAmount.value } returns BigDecimal.ONE
|
||||
every { reduceAmountBy } returns BigDecimal.ZERO
|
||||
every { isIgnoreReduce } returns false
|
||||
}
|
||||
val destination = mockk<DestinationUM.Content>(relaxed = true) {
|
||||
every { addressTextField.actualAddress } returns "destinationAddr"
|
||||
every { memoTextField } returns null
|
||||
every { wallets } returns persistentListOf()
|
||||
}
|
||||
val extraInfo = mockk<FeeExtraInfo>(relaxed = true) {
|
||||
every { this@mockk.transactionFeeExtended } returns transactionFeeExtended
|
||||
every { feeCryptoCurrencyStatus } returns loadedFeeStatus
|
||||
}
|
||||
val feeSelector = mockk<FeeSelectorUM.Content>(relaxed = true) {
|
||||
every { selectedFeeItem } returns FeeItem.Market(fee)
|
||||
every { feeNonce } returns FeeNonce.None
|
||||
every { feeExtraInfo } returns extraInfo
|
||||
every { isPrimaryButtonEnabled } returns true
|
||||
}
|
||||
return SendUM(
|
||||
amountUM = amount,
|
||||
destinationUM = destination,
|
||||
feeSelectorUM = feeSelector,
|
||||
confirmUM = mockk<ConfirmUM.Content>(relaxed = true),
|
||||
confirmData = null,
|
||||
)
|
||||
}
|
||||
|
||||
private val loadedFeeStatus: CryptoCurrencyStatus
|
||||
get() = com.tangem.features.send.loadedStatus(testCryptoCurrency)
|
||||
|
||||
// Can't reuse the shared commonFee(): it builds Amount(blockchain) whose value is null, and
|
||||
// verifyAndSendTransaction early-returns on `fee.amount.value ?: return` — so the fee needs an explicit value.
|
||||
private fun realFee(): Fee = Fee.Common(
|
||||
Amount(currencySymbol = "ETH", value = BigDecimal("0.001"), decimals = 18),
|
||||
)
|
||||
|
||||
private fun tokenFee(): Fee.Ethereum.TokenCurrency = Fee.Ethereum.TokenCurrency(
|
||||
amount = Amount(currencySymbol = "ETH", value = BigDecimal("0.001"), decimals = 18),
|
||||
gasLimit = java.math.BigInteger.valueOf(21_000),
|
||||
coinPriceInToken = java.math.BigInteger.ONE,
|
||||
feeTransferGasLimit = java.math.BigInteger.ONE,
|
||||
baseGas = java.math.BigInteger.ONE,
|
||||
)
|
||||
|
||||
data class OnSendClickModel(val elapsedRealtime: Long, val expectedSendInitiated: Boolean)
|
||||
|
||||
data class CheckFeeResultModel(val checkResult: Boolean, val expectedSendInitiated: Boolean)
|
||||
|
||||
data class DispatchModel(val isTokenCurrencyFee: Boolean)
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -10,12 +10,12 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.features.send.api.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.api.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.entity.FeeFiatRateUM
|
||||
import com.tangem.features.send.api.entity.FeeItem
|
||||
import com.tangem.features.send.api.entity.FeeNonce
|
||||
import com.tangem.features.send.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.CustomFeeFieldUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeExtraInfo
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeFiatRateUM
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeItem
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeNonce
|
||||
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformerV2
|
||||
import io.mockk.mockk
|
||||
|
|
@ -381,7 +381,7 @@ class NFTSendConfirmationNotificationsTransformerV2Test {
|
|||
isFeeApproximate = false,
|
||||
isFeeConvertibleToFiat = false,
|
||||
isTronToken = false,
|
||||
feeCryptoCurrencyStatus = cryptoCurrencyStatus
|
||||
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
),
|
||||
feeFiatRateUM = FeeFiatRateUM(
|
||||
rate = BigDecimal("50000"),
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue