Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-14 14:50:24 +03:00
commit 50fe9e603d
1919 changed files with 94940 additions and 15178 deletions

View file

@ -8,42 +8,44 @@ android {
namespace = "com.tangem.features.send.api"
}
dependencies {
/** Kotlin */
api(deps.kotlin.coroutines)
/** Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
implementation(projects.core.analytics.models)
api(projects.core.analytics.models)
api(projects.core.decompose)
api(projects.core.ui)
implementation(projects.core.utils)
/** Common */
implementation(projects.common.ui)
api(projects.common.ui)
/** Domain */
implementation(projects.domain.transaction)
api(projects.domain.transaction)
/** Domain models */
api(projects.domain.appCurrency.models)
api(projects.domain.models)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.nft.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.transaction.models)
implementation(projects.domain.wallets.models)
api(projects.domain.nft.models)
api(projects.domain.transaction.models)
api(projects.domain.wallets.models)
/** Compose */
api(deps.compose.foundation)
implementation(deps.compose.runtime)
implementation(deps.compose.foundation)
/** Tangem */
implementation(tangemDeps.blockchain)
api(tangemDeps.blockchain)
/** Other */
api(deps.kotlin.immutable.collections)
implementation(deps.androidx.annotation)
implementation(deps.arrow.core)
implementation(deps.kotlin.immutable.collections)
// region Tests
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit5)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
testImplementation(projects.common.test)
testImplementation(projects.domain.staking.models)
// endregion
}

View file

@ -1,3 +1,5 @@
package com.tangem.features.send.api
interface SendFeatureToggles
interface SendFeatureToggles {
val isHighFeeWarningEnabled: Boolean
}

View file

@ -1,7 +0,0 @@
package com.tangem.features.send.api.callbacks
import com.tangem.features.send.api.entity.FeeSelectorUM
interface FeeSelectorModelCallback {
fun onFeeResult(feeSelectorUM: FeeSelectorUM)
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.send.api.subcomponents.amount
import com.tangem.core.decompose.navigation.Route
/**
* Common route for amount
*/
interface AmountRoute : Route {
val isEditMode: Boolean
}

View file

@ -0,0 +1,19 @@
package com.tangem.features.send.api.subcomponents.amount
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
interface SendAmountBlockComponent : ComposableContentComponent {
fun updateState(amountUM: AmountState)
interface Factory {
fun create(
context: AppComponentContext,
params: SendAmountComponentParams.AmountBlockParams,
onClick: () -> Unit,
onResult: (AmountState) -> Unit,
): SendAmountBlockComponent
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.features.send.api.subcomponents.amount
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.navigationButtons.NavigationModelCallback
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.domain.wallets.models.errors.GetUserWalletError
interface SendAmountComponent : ComposableModularContentComponent {
fun updateState(amountUM: AmountState)
interface ModelCallback : NavigationModelCallback {
fun onAmountResult(amountUM: AmountState, isResetPredefined: Boolean)
fun onConvertToAnotherToken(lastAmount: String, isEnterInFiatSelected: Boolean)
fun resetSendNavigation()
fun onError(error: GetUserWalletError)
}
interface Factory : ComponentFactory<SendAmountComponentParams.AmountParams, SendAmountComponent>
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.subcomponents.amount
package com.tangem.features.send.api.subcomponents.amount
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.domain.appcurrency.model.AppCurrency
@ -9,10 +9,9 @@ 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 {
sealed class SendAmountComponentParams {
abstract val state: AmountState
abstract val analyticsCategoryName: String
@ -39,7 +38,7 @@ internal sealed class SendAmountComponentParams {
override val accountFlow: StateFlow<Account?>,
override val isAccountModeFlow: StateFlow<Boolean>,
val callback: SendAmountComponent.ModelCallback,
val currentRoute: StateFlow<CommonSendRoute>,
val route: AmountRoute,
) : SendAmountComponentParams()
data class AmountBlockParams(

View file

@ -0,0 +1,39 @@
package com.tangem.features.send.api.subcomponents.amount
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
import kotlinx.coroutines.flow.Flow
import java.math.BigDecimal
/**
* Trigger for reducing amount from another component
*/
interface SendAmountReduceTrigger {
suspend fun triggerReduceBy(reduceBy: AmountReduceByTransformer.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<AmountReduceByTransformer.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?>>
}

View file

@ -1,8 +1,10 @@
package com.tangem.features.send.api.subcomponents.destination
import com.tangem.core.decompose.navigation.Route
/**
* Common route for destination
*/
interface DestinationRoute {
interface DestinationRoute : Route {
val isEditMode: Boolean
}

View file

@ -2,10 +2,10 @@ package com.tangem.features.send.api.subcomponents.destination
import com.tangem.common.ui.navigationButtons.NavigationModelCallback
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.features.send.api.subcomponents.destination.entity.DestinationUM
interface SendDestinationComponent : ComposableContentComponent {
interface SendDestinationComponent : ComposableModularContentComponent {
fun updateState(destinationUM: DestinationUM)

View file

@ -6,7 +6,6 @@ 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.entity.DestinationUM
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
sealed class SendDestinationComponentParams {
@ -26,7 +25,7 @@ sealed class SendDestinationComponentParams {
override val userWalletId: UserWalletId,
val title: TextReference,
val isBalanceHidingFlow: StateFlow<Boolean>,
val currentRoute: Flow<DestinationRoute>,
val route: DestinationRoute,
val callback: SendDestinationComponent.ModelCallback,
override val isAllowSelfSend: Boolean = false,
) : SendDestinationComponentParams()
@ -40,5 +39,6 @@ sealed class SendDestinationComponentParams {
val blockClickEnableFlow: StateFlow<Boolean>,
val predefinedValues: PredefinedValues,
override val isAllowSelfSend: Boolean = false,
val isAddContactAvailable: Boolean = false,
) : SendDestinationComponentParams()
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.send.api.subcomponents.destination.entity
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.account.AccountIconUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.utils.toBriefAddressFormat
@ -24,6 +25,8 @@ sealed class DestinationTextFieldUM {
val isValuePasted: Boolean,
// if value is human-readable address, this field contains the actual blockchain address
val blockchainAddress: String? = null,
val contactName: String? = null,
val contactIcon: AccountIconUM.CryptoPortfolio? = null,
) : DestinationTextFieldUM() {
val actualAddress: String

View file

@ -1,9 +1,9 @@
package com.tangem.features.send.api
package com.tangem.features.send.api.subcomponents.feeSelector
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
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
interface FeeSelectorBlockComponent : ComposableContentComponent {

View file

@ -1,8 +1,8 @@
package com.tangem.features.send.api
package com.tangem.features.send.api.subcomponents.feeSelector
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.send.api.params.FeeSelectorParams
import com.tangem.features.send.api.subcomponents.feeSelector.params.FeeSelectorParams
interface FeeSelectorComponent : ComposableBottomSheetComponent {
interface Factory {

View file

@ -0,0 +1,7 @@
package com.tangem.features.send.api.subcomponents.feeSelector.callbacks
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
interface FeeSelectorModelCallback {
fun onFeeResult(feeSelectorUM: FeeSelectorUM)
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.api.entity
package com.tangem.features.send.api.subcomponents.feeSelector.entity
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.api.entity
package com.tangem.features.send.api.subcomponents.feeSelector.entity
import androidx.compose.runtime.Immutable
import com.tangem.blockchain.common.Amount

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.api.params
package com.tangem.features.send.api.subcomponents.feeSelector.params
import arrow.core.Either
import com.tangem.blockchain.common.transaction.Fee
@ -9,8 +9,8 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.models.TransactionFeeExtended
import com.tangem.features.send.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.api.callbacks.FeeSelectorModelCallback
import com.tangem.features.send.api.entity.FeeSelectorUM
import com.tangem.features.send.api.subcomponents.feeSelector.callbacks.FeeSelectorModelCallback
import com.tangem.features.send.api.subcomponents.feeSelector.entity.FeeSelectorUM
sealed class FeeSelectorParams {
abstract val state: FeeSelectorUM

View file

@ -4,8 +4,8 @@ import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
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.extensions.isZero
import java.math.BigDecimal
import java.math.RoundingMode

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.api
package com.tangem.features.send.api.subcomponents.notifications
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier

View file

@ -1,6 +1,5 @@
package com.tangem.features.send.api.subcomponents.notifications
import com.tangem.features.send.api.SendNotificationsComponent
import kotlinx.coroutines.flow.Flow
interface SendNotificationsUpdateListener {

View file

@ -1,7 +1,5 @@
package com.tangem.features.send.api.subcomponents.notifications
import com.tangem.features.send.api.SendNotificationsComponent
interface SendNotificationsUpdateTrigger {
/** Trigger return callback with check result */
suspend fun callbackHasError(hasError: Boolean)

View file

@ -17,6 +17,7 @@ dependencies {
implementation(projects.features.nft.api)
implementation(projects.features.swapV2.api)
implementation(projects.features.manageTokens.api)
implementation(projects.features.addressBook.api)
/** Libs */
implementation(projects.libs.crypto)
@ -68,6 +69,7 @@ dependencies {
implementation(projects.domain.swap.models)
implementation(projects.domain.account)
implementation(projects.domain.account.status)
implementation(projects.domain.addressBook)
implementation(projects.domain.transaction)
@ -87,12 +89,7 @@ dependencies {
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
// region Tests
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit5)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
testImplementation(projects.common.test)
// endregion
testImplementation(projects.test.core)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more