Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-02 11:51:19 +05:00
parent 8a81175461
commit 95f764c878
13 changed files with 354 additions and 155 deletions

View file

@ -0,0 +1,21 @@
package com.tangem.features.send.v2.common
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
internal sealed class NavigationUM {
data class Content(
val title: TextReference,
val subtitle: TextReference?,
@DrawableRes val backIconRes: Int,
val backIconClick: () -> Unit,
@DrawableRes val additionalIconRes: Int? = null,
val additionalIconClick: (() -> Unit)? = null,
val primaryButton: ButtonsUM.PrimaryButtonUM,
val prevButton: ButtonsUM.PrimaryButtonUM?,
val secondaryPairButtonsUM: ButtonsUM.SecondaryPairButtonsUM?,
) : NavigationUM()
data object Empty : NavigationUM()
}

View file

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

View file

@ -7,6 +7,7 @@ import com.tangem.features.send.v2.send.model.SendModel
import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel
import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeModel
import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationsModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -41,4 +42,9 @@ internal interface SendModelModule {
@IntoMap
@ClassKey(SendConfirmModel::class)
fun provideSendConfirmModel(model: SendConfirmModel): Model
@Binds
@IntoMap
@ClassKey(NotificationsModel::class)
fun provideNotificationsModel(model: NotificationsModel): Model
}

View file

@ -15,6 +15,9 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
@ -34,6 +37,8 @@ import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource
@ -479,71 +484,70 @@ internal class SendConfirmModel @Inject constructor(
}
}
fun configConfirmNavigation() {
private fun configConfirmNavigation() {
combine(
flow = uiState,
flow2 = params.currentRoute,
transform = { state, route -> state to route },
).onEach { (state, _) ->
// todo
// val amountUM = state.amountUM as? AmountState.Data
// val confirmUM = state.confirmUM
// params.callback.onResult(
// state.copy(
// navigationUM = NavigationUM.Content(
// title = resourceReference(
// id = R.string.send_summary_title,
// formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name),
// ),
// subtitle = amountUM?.title,
// backIconRes = R.drawable.ic_close_24,
// backIconClick = {
// analyticsEventHandler.send(
// SendAnalyticEvents.CloseButtonClicked(
// source = SendScreenSource.Confirm,
// isFromSummary = true,
// isValid = confirmUM.isPrimaryButtonEnabled,
// ),
// )
// appRouter.pop()
// },
// primaryButton = ButtonsUM.PrimaryButtonUM(
// text = when (confirmUM) {
// is ConfirmUM.Success -> resourceReference(R.string.common_close)
// is ConfirmUM.Content -> if (confirmUM.isSending) {
// resourceReference(R.string.send_sending)
// } else {
// resourceReference(R.string.common_send)
// }
// else -> resourceReference(R.string.common_send)
// },
// iconResId = R.drawable.ic_tangem_24.takeIf { confirmUM is ConfirmUM.Content },
// isEnabled = confirmUM.isPrimaryButtonEnabled,
// isHapticClick = confirmUM is ConfirmUM.Content && !confirmUM.isSending,
// onClick = {
// when (confirmUM) {
// is ConfirmUM.Success -> appRouter.pop()
// is ConfirmUM.Content -> if (confirmUM.isSending) {
// return@PrimaryButtonUM
// } else {
// onSendClick()
// }
// else -> return@PrimaryButtonUM
// }
// },
// ),
// prevButton = null,
// secondaryPairButtonsUM = ButtonsUM.SecondaryPairButtonsUM(
// leftText = resourceReference(R.string.common_explore),
// leftIconResId = R.drawable.ic_web_24,
// onLeftClick = ::onExploreClick,
// rightText = resourceReference(R.string.common_share),
// rightIconResId = R.drawable.ic_share_24,
// onRightClick = ::onShareClick,
// ).takeIf { confirmUM is ConfirmUM.Success },
// ),
// ),
// )
val amountUM = state.amountUM as? AmountState.Data
val confirmUM = state.confirmUM
params.callback.onResult(
state.copy(
navigationUM = NavigationUM.Content(
title = resourceReference(
id = R.string.send_summary_title,
formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name),
),
subtitle = amountUM?.title,
backIconRes = R.drawable.ic_close_24,
backIconClick = {
analyticsEventHandler.send(
SendAnalyticEvents.CloseButtonClicked(
source = SendScreenSource.Confirm,
isFromSummary = true,
isValid = confirmUM.isPrimaryButtonEnabled,
),
)
appRouter.pop()
},
primaryButton = ButtonsUM.PrimaryButtonUM(
text = when (confirmUM) {
is ConfirmUM.Success -> resourceReference(R.string.common_close)
is ConfirmUM.Content -> if (confirmUM.isSending) {
resourceReference(R.string.send_sending)
} else {
resourceReference(R.string.common_send)
}
else -> resourceReference(R.string.common_send)
},
iconResId = R.drawable.ic_tangem_24.takeIf { confirmUM is ConfirmUM.Content },
isEnabled = confirmUM.isPrimaryButtonEnabled,
isHapticClick = confirmUM is ConfirmUM.Content && !confirmUM.isSending,
onClick = {
when (confirmUM) {
is ConfirmUM.Success -> appRouter.pop()
is ConfirmUM.Content -> if (confirmUM.isSending) {
return@PrimaryButtonUM
} else {
onSendClick()
}
else -> return@PrimaryButtonUM
}
},
),
prevButton = null,
secondaryPairButtonsUM = ButtonsUM.SecondaryPairButtonsUM(
leftText = resourceReference(R.string.common_explore),
leftIconResId = R.drawable.ic_web_24,
onLeftClick = ::onExploreClick,
rightText = resourceReference(R.string.common_share),
rightIconResId = R.drawable.ic_share_24,
onRightClick = ::onShareClick,
).takeIf { confirmUM is ConfirmUM.Success },
),
),
)
}.launchIn(modelScope)
}

View file

@ -0,0 +1,128 @@
package com.tangem.features.send.v2.send.ui
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.v2.common.NavigationUM
@Composable
internal fun SendNavigationButtons(navigationUM: NavigationUM, modifier: Modifier = Modifier) {
val navigationUM = navigationUM as? NavigationUM.Content ?: return
Column(
modifier = modifier.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
) {
SendDoneButtons(navigationUM.secondaryPairButtonsUM)
SendNavigationButton(
navigationUM = navigationUM,
)
}
}
@Composable
private fun SendNavigationButton(navigationUM: NavigationUM, modifier: Modifier = Modifier) {
val hapticFeedback = LocalHapticFeedback.current
val navigationUM = navigationUM as? NavigationUM.Content ?: return
val primaryButton = navigationUM.primaryButton
Row(modifier = modifier) {
AnimatedVisibility(
visible = navigationUM.prevButton != null,
enter = expandHorizontally(expandFrom = Alignment.End),
exit = shrinkHorizontally(shrinkTowards = Alignment.End),
) {
val wrappedNavigationUM = remember(this) { requireNotNull(navigationUM.prevButton) }
Row {
Icon(
painter = rememberVectorPainter(ImageVector.vectorResource(R.drawable.ic_back_24)),
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.background(TangemTheme.colors.button.secondary)
.clickable(onClick = wrappedNavigationUM.onClick)
.padding(12.dp),
)
SpacerW12()
}
}
TangemButton(
modifier = Modifier.fillMaxWidth(),
text = primaryButton.text.resolveReference(),
icon = primaryButton.iconResId?.let {
TangemButtonIconPosition.End(it)
} ?: TangemButtonIconPosition.None,
enabled = primaryButton.isEnabled,
onClick = {
if (primaryButton.isHapticClick) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
primaryButton.onClick()
},
showProgress = false,
colors = TangemButtonsDefaults.primaryButtonColors,
textStyle = TangemTheme.typography.subtitle1,
)
}
}
@Composable
private fun SendDoneButtons(pairButtonsUM: ButtonsUM.SecondaryPairButtonsUM?, modifier: Modifier = Modifier) {
val hapticFeedback = LocalHapticFeedback.current
AnimatedVisibility(
visible = pairButtonsUM != null,
modifier = modifier,
enter = slideInVertically().plus(fadeIn()),
exit = slideOutVertically().plus(fadeOut()),
label = "Animate show sent state buttons",
) {
val wrappedPairButtonsUM = remember(this) { requireNotNull(pairButtonsUM) }
Row(modifier = Modifier.padding(bottom = 12.dp)) {
SecondaryButtonIconStart(
text = wrappedPairButtonsUM.leftText.resolveReference(),
iconResId = wrappedPairButtonsUM.leftIconResId!!,
onClick = wrappedPairButtonsUM.onLeftClick,
modifier = Modifier.weight(1f),
)
SpacerW12()
SecondaryButtonIconStart(
text = wrappedPairButtonsUM.rightText.resolveReference(),
iconResId = wrappedPairButtonsUM.rightIconResId!!,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
wrappedPairButtonsUM.onRightClick()
},
modifier = Modifier.weight(1f),
)
}
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.features.send.v2.send.ui.state
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.TextReference
sealed class ButtonsUM {
data class PrimaryButtonUM(
val text: TextReference,
val additionalText: TextReference? = null,
@DrawableRes val iconResId: Int? = null,
val isEnabled: Boolean,
val isHapticClick: Boolean = false,
val onClick: () -> Unit,
) : ButtonsUM()
data class SecondaryPairButtonsUM(
val leftText: TextReference,
@DrawableRes val leftIconResId: Int? = null,
val onLeftClick: () -> Unit,
val rightText: TextReference,
@DrawableRes val rightIconResId: Int? = null,
val onRightClick: () -> Unit,
) : ButtonsUM()
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.send.v2.send.ui.state
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
@ -10,4 +11,5 @@ internal data class SendUM(
val destinationUM: DestinationUM,
val feeUM: FeeUM,
val confirmUM: ConfirmUM,
val navigationUM: NavigationUM,
)

View file

@ -11,6 +11,7 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.v2.common.SendNavigationModelCallback
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams.AmountParams
import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel
@ -35,7 +36,7 @@ internal class SendAmountComponent(
)
}
interface ModelCallback {
interface ModelCallback : SendNavigationModelCallback {
fun onAmountResult(amountUM: AmountState)
}
}

View file

@ -16,6 +16,8 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams
import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener
@ -200,40 +202,39 @@ internal class SendAmountModel @Inject constructor(
flow = uiState,
flow2 = params.currentRoute,
transform = { state, route -> state to route },
).onEach { (_, _) ->
// todo
// params.callback.onNavigationResult(
// NavigationUM.Content(
// title = resourceReference(R.string.send_amount_label),
// subtitle = null,
// backIconRes = R.drawable.ic_back_24,
// backIconClick = {
// if (route.isEditMode) {
// saveResult()
// }
// router.pop()
// },
// primaryButton = ButtonsUM.PrimaryButtonUM(
// text = if (route.isEditMode) {
// resourceReference(R.string.common_continue)
// } else {
// resourceReference(R.string.common_next)
// },
// isEnabled = state.isPrimaryButtonEnabled,
// onClick = ::onAmountNext,
// ),
// prevButton = ButtonsUM.PrimaryButtonUM(
// text = TextReference.EMPTY,
// iconResId = R.drawable.ic_back_24,
// isEnabled = true,
// onClick = {
// saveResult()
// router.pop()
// },
// ).takeIf { route.isEditMode.not() },
// secondaryPairButtonsUM = null,
// ),
// )
).onEach { (state, route) ->
params.callback.onNavigationResult(
NavigationUM.Content(
title = resourceReference(R.string.send_amount_label),
subtitle = null,
backIconRes = R.drawable.ic_back_24,
backIconClick = {
if (!route.isEditMode) {
saveResult()
}
router.pop()
},
primaryButton = ButtonsUM.PrimaryButtonUM(
text = if (route.isEditMode) {
resourceReference(R.string.common_continue)
} else {
resourceReference(R.string.common_next)
},
isEnabled = state.isPrimaryButtonEnabled,
onClick = ::onAmountNext,
),
prevButton = ButtonsUM.PrimaryButtonUM(
text = TextReference.EMPTY,
iconResId = R.drawable.ic_back_24,
isEnabled = true,
onClick = {
saveResult()
router.pop()
},
).takeIf { route.isEditMode.not() },
secondaryPairButtonsUM = null,
),
)
}.launchIn(modelScope)
}
}

View file

@ -40,7 +40,7 @@ internal class SendDestinationComponent(
val isEditMode: Boolean,
)
interface ModelCallback {
interface ModelCallback : SendNavigationModelCallback {
fun onDestinationResult(destinationUM: DestinationUM)
}
}

View file

@ -19,6 +19,8 @@ import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent
import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource
@ -44,6 +46,7 @@ internal class SendDestinationModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val appRouter: AppRouter,
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
@ -254,46 +257,45 @@ internal class SendDestinationModel @Inject constructor(
flow = uiState,
flow2 = params.currentRoute,
transform = { state, route -> state to route },
).onEach {
// todo
// params.callback.onNavigationResult(
// NavigationUM.Content(
// title = resourceReference(R.string.send_recipient_label),
// subtitle = null,
// backIconRes = if (route.isEditMode) {
// R.drawable.ic_back_24
// } else {
// R.drawable.ic_close_24
// },
// backIconClick = {
// if (route.isEditMode) {
// router.pop()
// } else {
// appRouter.pop()
// }
// },
// additionalIconRes = R.drawable.ic_qrcode_scan_24,
// additionalIconClick = {
// router.push(
// AppRoute.QrScanning(
// source = AppRoute.QrScanning.Source.SEND,
// networkName = cryptoCurrency.network.name,
// ),
// )
// },
// primaryButton = ButtonsUM.PrimaryButtonUM(
// text = if (route.isEditMode) {
// resourceReference(R.string.common_continue)
// } else {
// resourceReference(R.string.common_next)
// },
// isEnabled = state.isPrimaryButtonEnabled,
// onClick = ::onNextClick,
// ),
// prevButton = null,
// secondaryPairButtonsUM = null,
// ),
// )
).onEach { (state, route) ->
params.callback.onNavigationResult(
NavigationUM.Content(
title = resourceReference(R.string.send_recipient_label),
subtitle = null,
backIconRes = if (route.isEditMode) {
R.drawable.ic_back_24
} else {
R.drawable.ic_close_24
},
backIconClick = {
if (route.isEditMode) {
router.pop()
} else {
analyticsEventHandler.send(
SendAnalyticEvents.CloseButtonClicked(
source = SendScreenSource.Address,
isFromSummary = false,
isValid = state.isPrimaryButtonEnabled,
),
)
appRouter.pop()
}
},
additionalIconRes = R.drawable.ic_qrcode_scan_24,
additionalIconClick = ::onQrCodeScanClick,
primaryButton = ButtonsUM.PrimaryButtonUM(
text = if (route.isEditMode) {
resourceReference(R.string.common_continue)
} else {
resourceReference(R.string.common_next)
},
isEnabled = state.isPrimaryButtonEnabled,
onClick = ::onNextClick,
),
prevButton = null,
secondaryPairButtonsUM = null,
),
)
}.launchIn(modelScope)
}

View file

@ -6,9 +6,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.features.send.v2.common.SendNavigationModelCallback
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeModel
import com.tangem.features.send.v2.subcomponents.fee.ui.SendFeeContent
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
internal class SendFeeComponent(
appComponentContext: AppComponentContext,
@ -24,7 +25,7 @@ internal class SendFeeComponent(
SendFeeContent(state = state.value, clickIntents = model)
}
interface ModelCallback {
interface ModelCallback : SendNavigationModelCallback {
fun onFeeResult(feeUM: FeeUM)
}
}

View file

@ -8,8 +8,12 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams
import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger
@ -261,23 +265,22 @@ internal class SendFeeModel @Inject constructor(
flow = uiState,
flow2 = params.currentRoute,
transform = { state, route -> state to route },
).onEach { (_, _) ->
// todo
// params.callback.onNavigationResult(
// NavigationUM.Content(
// title = resourceReference(R.string.common_fee_selector_title),
// subtitle = null,
// backIconRes = R.drawable.ic_back_24,
// backIconClick = router::pop,
// primaryButton = ButtonsUM.PrimaryButtonUM(
// text = resourceReference(R.string.common_continue),
// isEnabled = state.isPrimaryButtonEnabled,
// onClick = ::onNextClick,
// ),
// prevButton = null,
// secondaryPairButtonsUM = null,
// ),
// )
).onEach { (state, _) ->
params.callback.onNavigationResult(
NavigationUM.Content(
title = resourceReference(R.string.common_fee_selector_title),
subtitle = null,
backIconRes = R.drawable.ic_back_24,
backIconClick = router::pop,
primaryButton = ButtonsUM.PrimaryButtonUM(
text = resourceReference(R.string.common_continue),
isEnabled = state.isPrimaryButtonEnabled,
onClick = ::onNextClick,
),
prevButton = null,
secondaryPairButtonsUM = null,
),
)
}.launchIn(modelScope)
}