Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-24 16:33:38 +03:00
parent 33406d0ac1
commit 2a9a80414d
39 changed files with 1074 additions and 499 deletions

View file

@ -1,5 +1,6 @@
package com.tangem.features.send.impl.di
import com.tangem.core.navigation.ReduxNavController
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.navigation.DefaultSendRouter
import dagger.Module
@ -17,7 +18,7 @@ internal object SendRouterModule {
@Provides
@ActivityScoped
fun provideSendRouter(): SendRouter {
return DefaultSendRouter()
fun provideSendRouter(reduxNavController: ReduxNavController): SendRouter {
return DefaultSendRouter(reduxNavController)
}
}

View file

@ -1,9 +1,17 @@
package com.tangem.features.send.impl.navigation
import androidx.fragment.app.Fragment
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.ReduxNavController
import com.tangem.features.send.impl.presentation.SendFragment
internal class DefaultSendRouter : SendRouter {
internal class DefaultSendRouter(
private val reduxNavController: ReduxNavController,
) : InnerSendRouter {
override fun getEntryFragment(): Fragment = SendFragment.create()
override fun openUrl(url: String) {
reduxNavController.navigate(NavigationAction.OpenUrl(url = url))
}
}

View file

@ -0,0 +1,9 @@
package com.tangem.features.send.impl.navigation
import com.tangem.features.send.api.navigation.SendRouter
interface InnerSendRouter : SendRouter {
/** Open website by [url] */
fun openUrl(url: String)
}

View file

@ -8,6 +8,8 @@ import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.navigation.InnerSendRouter
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.features.send.impl.presentation.ui.SendScreen
import com.tangem.features.send.impl.presentation.viewmodel.SendViewModel
@ -24,12 +26,20 @@ internal class SendFragment : ComposeFragment() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
@Inject
lateinit var router: SendRouter
private val viewModel by viewModels<SendViewModel>()
private val innerSendRouter: InnerSendRouter
get() = requireNotNull(router as? InnerSendRouter) {
"innerSendRouter should be instance of InnerSendRouter"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(viewModel)
viewModel.setRouter(
innerSendRouter,
StateRouter(
fragmentManager = WeakReference(parentFragmentManager),
),

View file

@ -0,0 +1,10 @@
package com.tangem.features.send.impl.presentation.domain
sealed class SendNotification {
sealed class Info(val message: String) : SendNotification()
sealed class Critical(val message: String) : SendNotification()
sealed class Error(val message: String) : SendNotification()
}

View file

@ -86,6 +86,7 @@ internal class SendStateFactory(
amountState = amountStateConverter.convert(Unit),
recipientState = recipientStateConverter.convert(Unit),
feeState = feeStateConverter.convert(Unit),
sendState = SendStates.SendState(),
)
//endregion
@ -122,15 +123,15 @@ internal class SendStateFactory(
val recipientState = state.recipientState ?: return state
val isValidMemo = validateMemo(
memo = value,
memo = recipientState.addressTextField.value.value,
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
)
val isAddressInWallet = isNotAddressInWallet(
address = value,
walletAddresses = walletAddressesProvider(),
address = recipientState.addressTextField.value.value,
)
val isValidAddress = verifyAddress(
address = recipientState.addressTextField.value.value,
address = value,
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
)
@ -138,8 +139,7 @@ internal class SendStateFactory(
it.copy(
value = value,
error = when {
!isValidAddress -> TextReference.Res(R.string.send_recipient_address_error)
!isAddressInWallet -> TextReference.Res(R.string.send_recipient_address_error)
!isValidAddress || !isAddressInWallet -> TextReference.Res(R.string.send_recipient_address_error)
else -> null
},
isError = !isValidAddress || !isAddressInWallet,
@ -169,11 +169,9 @@ internal class SendStateFactory(
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
)
// todo add memo validation error text
recipientState.memoTextField?.update {
it.copy(
value = value,
error = TextReference.Res(R.string.send_memo_destination_tag_error),
isError = !isValidMemo,
)
}

View file

@ -23,6 +23,7 @@ internal data class SendUiState(
val amountState: SendStates.AmountState? = null,
val recipientState: SendStates.RecipientState? = null,
val feeState: SendStates.FeeState? = null,
val sendState: SendStates.SendState? = null,
val recipientList: MutableStateFlow<PagingData<SendRecipientListContent>> = MutableStateFlow(PagingData.empty()),
val currentState: MutableStateFlow<SendUiStateType>,
)
@ -65,11 +66,14 @@ internal sealed class SendStates {
val receivedAmount: MutableStateFlow<String> = MutableStateFlow(""),
) : SendStates()
// todo [REDACTED_JIRA]
/** Send state */
data class SendState(
val isSuccess: Boolean,
)
override val type: SendUiStateType = SendUiStateType.Send,
val isSending: MutableStateFlow<Boolean> = MutableStateFlow(false),
val isSuccess: MutableStateFlow<Boolean> = MutableStateFlow(false),
val transactionDate: MutableStateFlow<Long> = MutableStateFlow(0L),
val txUrl: MutableStateFlow<String> = MutableStateFlow(""),
) : SendStates()
}
enum class SendUiStateType {
@ -77,5 +81,4 @@ enum class SendUiStateType {
Recipient,
Fee,
Send,
Done,
}

View file

@ -9,28 +9,61 @@ internal class StateRouter(
private val fragmentManager: WeakReference<FragmentManager>,
) {
var currentState: MutableStateFlow<SendUiStateType> = MutableStateFlow(SendUiStateType.Amount)
private set
private var isFromSend: Boolean = false
fun popBackStack() {
fragmentManager.get()?.popBackStack()
}
fun onBackClick() {
fragmentManager.get()?.popBackStack()
if (isFromSend) {
showSend()
} else {
when (currentState.value) {
SendUiStateType.Amount -> popBackStack()
SendUiStateType.Recipient -> showAmount()
SendUiStateType.Fee -> showRecipient()
SendUiStateType.Send -> showFee()
}
}
}
fun onNextClick() {
when (currentState.value) {
SendUiStateType.Amount -> currentState.update { SendUiStateType.Recipient }
SendUiStateType.Recipient -> currentState.update { SendUiStateType.Fee }
SendUiStateType.Fee -> currentState.update { SendUiStateType.Send }
SendUiStateType.Send -> currentState.update { SendUiStateType.Done }
SendUiStateType.Done -> onBackClick()
SendUiStateType.Amount -> showRecipient()
SendUiStateType.Recipient -> showFee()
SendUiStateType.Fee -> showSend()
SendUiStateType.Send -> onBackClick()
}
}
fun onPrevClick() {
when (currentState.value) {
SendUiStateType.Amount -> onBackClick()
SendUiStateType.Recipient -> currentState.update { SendUiStateType.Amount }
SendUiStateType.Fee -> currentState.update { SendUiStateType.Recipient }
SendUiStateType.Send -> currentState.update { SendUiStateType.Fee }
SendUiStateType.Done -> onBackClick()
SendUiStateType.Amount -> popBackStack()
SendUiStateType.Recipient -> showAmount()
SendUiStateType.Fee -> showRecipient()
SendUiStateType.Send -> popBackStack()
}
}
fun showAmount(isFromSend: Boolean = false) {
this.isFromSend = isFromSend
currentState.update { SendUiStateType.Amount }
}
fun showRecipient(isFromSend: Boolean = false) {
this.isFromSend = isFromSend
currentState.update { SendUiStateType.Recipient }
}
fun showFee(isFromSend: Boolean = false) {
this.isFromSend = isFromSend
currentState.update { SendUiStateType.Fee }
}
fun showSend() {
currentState.update { SendUiStateType.Send }
}
}

View file

@ -45,6 +45,7 @@ internal class SendRecipientMemoFieldConverter(
),
placeholder = TextReference.Res(R.string.send_optional_field),
label = TextReference.Res(value),
error = TextReference.Res(R.string.send_memo_destination_tag_error),
),
)
}

View file

@ -1,23 +1,30 @@
package com.tangem.features.send.impl.presentation.ui
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
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.State
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.R
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.*
import com.tangem.core.ui.extensions.shareText
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiStateType
@ -64,16 +71,16 @@ private fun SendSecondaryNavigationButton(uiState: SendUiState) {
@Composable
private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier = Modifier) {
val currentState = uiState.currentState.collectAsState()
val currentState = uiState.currentState.collectAsStateWithLifecycle()
val isSuccess = uiState.sendState?.isSuccess?.collectAsStateWithLifecycle()?.value ?: false
val isSending = uiState.sendState?.isSending?.collectAsStateWithLifecycle()?.value ?: false
val txUrl = uiState.sendState?.txUrl?.collectAsStateWithLifecycle()?.value.orEmpty()
val buttonTextId = when (currentState.value) {
SendUiStateType.Amount,
SendUiStateType.Recipient,
SendUiStateType.Fee,
-> R.string.common_next
SendUiStateType.Send -> R.string.common_send
else -> R.string.common_close
}
val (buttonTextId, buttonClick) = getButtonData(
currentState = currentState,
isSuccess = isSuccess,
uiState = uiState,
)
val isButtonEnabled = when (currentState.value) {
SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false
@ -86,19 +93,92 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier
label = "Update send screen state",
modifier = modifier,
) { textId ->
if (currentState.value == SendUiStateType.Send) {
PrimaryButtonIconEnd(
text = stringResource(textId),
iconResId = R.drawable.ic_tangem_24,
enabled = isButtonEnabled,
onClick = uiState.clickIntents::onNextClick,
)
} else {
PrimaryButton(
text = stringResource(textId),
enabled = isButtonEnabled,
onClick = uiState.clickIntents::onNextClick,
)
when {
currentState.value == SendUiStateType.Send && !isSuccess -> {
PrimaryButtonIconEnd(
text = stringResource(textId),
iconResId = R.drawable.ic_tangem_24,
enabled = isButtonEnabled,
onClick = buttonClick,
showProgress = isSending,
)
}
currentState.value == SendUiStateType.Send && isSuccess -> {
PrimaryButtonsDone(
textRes = textId,
txUrl = txUrl,
onExploreClick = { uiState.clickIntents.onExploreClick(txUrl) },
onDoneClick = buttonClick,
modifier = Modifier,
)
}
else -> {
PrimaryButton(
text = stringResource(textId),
enabled = isButtonEnabled,
onClick = buttonClick,
)
}
}
}
}
@Composable
private fun PrimaryButtonsDone(
@StringRes textRes: Int,
txUrl: String,
onExploreClick: () -> Unit,
onDoneClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val hapticFeedback = LocalHapticFeedback.current
val context = LocalContext.current
Column(modifier = modifier) {
if (txUrl.isNotBlank()) {
Row {
SecondaryButtonIconStart(
text = stringResource(id = R.string.common_explore),
iconResId = R.drawable.ic_web_24,
onClick = onExploreClick,
modifier = Modifier.weight(1f),
)
SpacerW12()
SecondaryButtonIconStart(
text = stringResource(id = R.string.common_share),
iconResId = R.drawable.ic_share_24,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
context.shareText(txUrl)
},
modifier = Modifier.weight(1f),
)
}
SpacerH12()
}
PrimaryButton(
text = stringResource(id = textRes),
enabled = true,
onClick = onDoneClick,
modifier = Modifier.fillMaxWidth(),
)
}
}
private fun getButtonData(
uiState: SendUiState,
currentState: State<SendUiStateType>,
isSuccess: Boolean,
): Pair<Int, () -> Unit> {
return when (currentState.value) {
SendUiStateType.Amount,
SendUiStateType.Recipient,
SendUiStateType.Fee,
-> R.string.common_next to uiState.clickIntents::onNextClick
SendUiStateType.Send -> if (isSuccess) {
R.string.common_close
} else {
R.string.common_send
} to uiState.clickIntents::onSendClick
}
}

View file

@ -22,11 +22,13 @@ import com.tangem.features.send.impl.presentation.state.SendUiStateType
import com.tangem.features.send.impl.presentation.ui.amount.SendAmountContent
import com.tangem.features.send.impl.presentation.ui.fee.SendSpeedAndFeeContent
import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent
import com.tangem.features.send.impl.presentation.ui.send.SendContent
@Composable
internal fun SendScreen(uiState: SendUiState) {
val currentState = uiState.currentState.collectAsStateWithLifecycle()
BackHandler { uiState.clickIntents.onPrevClick() }
val isSuccess = uiState.sendState?.isSuccess?.collectAsStateWithLifecycle()
BackHandler { uiState.clickIntents.onBackClick() }
Column(
modifier = Modifier
.fillMaxSize()
@ -36,12 +38,10 @@ internal fun SendScreen(uiState: SendUiState) {
horizontalAlignment = Alignment.CenterHorizontally,
) {
val titleRes = when (currentState.value) {
SendUiStateType.Amount,
SendUiStateType.Send,
-> R.string.common_send
SendUiStateType.Amount -> R.string.common_send
SendUiStateType.Recipient -> R.string.send_recipient
SendUiStateType.Fee -> R.string.common_fee_selector_title
SendUiStateType.Done -> null
SendUiStateType.Send -> if (isSuccess?.value == false) R.string.common_send else null
}
val iconRes = when (currentState.value) {
SendUiStateType.Amount,
@ -52,7 +52,7 @@ internal fun SendScreen(uiState: SendUiState) {
AppBarWithBackButtonAndIcon(
text = titleRes?.let { stringResource(it) },
onBackClick = uiState.clickIntents::onBackClick,
onBackClick = uiState.clickIntents::popBackStack,
onIconClick = uiState.clickIntents::onQrCodeScanClick,
backIconRes = R.drawable.ic_close_24,
iconRes = iconRes,
@ -94,7 +94,7 @@ private fun SendScreenContent(
uiState.feeState,
uiState.clickIntents,
)
else -> { /* [REDACTED_TODO_COMMENT]*/ }
SendUiStateType.Send -> SendContent(uiState)
}
}
}

View file

@ -19,7 +19,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.text.style.TextAlign
import com.tangem.core.ui.components.fields.AmountVisualTransformation
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme

View file

@ -1,17 +1,21 @@
package com.tangem.features.send.impl.presentation.ui.fee
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.fields.AmountVisualTransformation
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
import com.tangem.core.ui.components.inputrow.InputRowEnter
import com.tangem.core.ui.components.inputrow.InputRowEnterInfo
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.ui.recipient.TextFieldWithInfo
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
private const val ETHEREUM_UNIT = "GWEI"
@ -22,42 +26,66 @@ internal fun SendCustomFeeEthereum(
symbol: String,
modifier: Modifier = Modifier,
) {
val fee = customValues.value[0]
val gasPrice = customValues.value[1]
val gasLimit = customValues.value[2]
if (selectedFee == FeeType.CUSTOM && customValues.value.isNotEmpty()) {
val fee = customValues.value[0]
val gasPrice = customValues.value[1]
val gasLimit = customValues.value[2]
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
modifier = modifier,
) {
TextFieldWithInfo(
value = fee.value,
label = stringResource(R.string.send_max_fee),
FooterContainer(
footer = stringResource(R.string.send_max_fee_footer),
info = fee.label,
visualTransformation = AmountVisualTransformation(symbol),
keyboardOptions = fee.keyboardOptions,
onValueChange = fee.onValueChange,
isSingleLine = true,
)
TextFieldWithInfo(
value = gasPrice.value,
label = stringResource(R.string.send_gas_price),
) {
InputRowEnterInfo(
text = fee.value,
title = TextReference.Res(R.string.send_max_fee),
info = fee.label,
visualTransformation = AmountVisualTransformation(symbol),
keyboardOptions = fee.keyboardOptions,
onValueChange = fee.onValueChange,
isSingleLine = true,
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
)
}
FooterContainer(
footer = stringResource(R.string.send_gas_price_footer),
onValueChange = gasPrice.onValueChange,
visualTransformation = AmountVisualTransformation(ETHEREUM_UNIT),
keyboardOptions = fee.keyboardOptions,
isSingleLine = true,
)
TextFieldWithInfo(
value = gasLimit.value,
label = stringResource(R.string.send_gas_limit),
) {
InputRowEnter(
text = gasPrice.value,
title = TextReference.Res(R.string.send_gas_price),
onValueChange = gasPrice.onValueChange,
visualTransformation = AmountVisualTransformation(ETHEREUM_UNIT),
keyboardOptions = fee.keyboardOptions,
isSingleLine = true,
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
)
}
FooterContainer(
footer = stringResource(R.string.send_gas_limit_footer),
onValueChange = gasLimit.onValueChange,
keyboardOptions = fee.keyboardOptions,
isSingleLine = true,
)
) {
InputRowEnter(
text = gasLimit.value,
title = TextReference.Res(R.string.send_gas_limit),
onValueChange = gasLimit.onValueChange,
keyboardOptions = fee.keyboardOptions,
isSingleLine = true,
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
)
}
}
}
}

View file

@ -18,11 +18,13 @@ import androidx.compose.ui.res.stringResource
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import com.tangem.core.ui.components.inputrow.InputRowRecipient
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY"
@ -45,18 +47,26 @@ internal fun SendRecipientContent(
.padding(horizontal = TangemTheme.dimens.spacing16),
) {
item(key = ADDRESS_FIELD_KEY) {
TextFieldWithPasteAndIcon(
value = address.value,
label = address.label,
placeholder = address.placeholder,
FooterContainer(
footer = stringResource(R.string.send_recipient_address_footer, uiState.network),
onValueChange = address.onValueChange,
onPasteClick = clickIntents::onRecipientAddressValueChange,
singleLine = true,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing4),
isError = address.isError,
error = address.error,
)
) {
InputRowRecipient(
value = address.value,
title = address.label,
placeholder = address.placeholder,
onValueChange = address.onValueChange,
onPasteClick = clickIntents::onRecipientAddressValueChange,
singleLine = true,
isError = address.isError,
error = address.error,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing4)
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
)
}
}
memo?.let { memoField ->
item(key = MEMO_FIELD_KEY) {

View file

@ -0,0 +1,70 @@
package com.tangem.features.send.impl.presentation.ui.recipient
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.fields.SimpleTextField
import com.tangem.core.ui.components.inputrow.inner.PasteButton
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.impl.presentation.ui.common.FooterContainer
@Composable
internal fun TextFieldWithPaste(
value: String,
placeholder: TextReference,
label: TextReference,
onValueChange: (String) -> Unit,
onPasteClick: (String) -> Unit,
modifier: Modifier = Modifier,
footer: String? = null,
error: TextReference? = null,
isError: Boolean = false,
) {
val (title, color) = if (isError && error != null) {
error to TangemTheme.colors.text.warning
} else {
label to TangemTheme.colors.text.secondary
}
FooterContainer(modifier, footer) {
Row(
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
) {
Column(
modifier = Modifier
.weight(1f)
.padding(TangemTheme.dimens.spacing12),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.body2,
color = color,
)
SimpleTextField(
value = value,
placeholder = placeholder,
onValueChange = onValueChange,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing6),
)
}
PasteButton(
isPasteButtonVisible = value.isBlank(),
onClick = onPasteClick,
modifier = Modifier
.align(CenterVertically)
.padding(end = TangemTheme.dimens.spacing16),
)
}
}
}

View file

@ -1,385 +0,0 @@
package com.tangem.features.send.impl.presentation.ui.recipient
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.icons.identicon.IdentIcon
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.impl.R
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
@Composable
internal fun TextFieldWithPasteAndIcon(
value: String,
placeholder: TextReference,
label: TextReference,
onValueChange: (String) -> Unit,
onPasteClick: (String) -> Unit,
modifier: Modifier = Modifier,
footer: String? = null,
singleLine: Boolean = false,
error: TextReference? = null,
isError: Boolean = false,
) {
val (title, color) = if (isError && error != null) {
error to TangemTheme.colors.text.warning
} else {
label to TangemTheme.colors.text.secondary
}
FooterContainer(modifier, footer) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.body2,
color = color,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing12,
),
)
Row {
IdentIcon(
address = value,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
top = TangemTheme.dimens.spacing8,
bottom = TangemTheme.dimens.spacing10,
)
.clip(RoundedCornerShape(TangemTheme.dimens.radius20))
.size(TangemTheme.dimens.size40)
.background(TangemTheme.colors.background.tertiary),
)
SimpleTextField(
value = value,
placeholder = placeholder,
onValueChange = onValueChange,
singleLine = singleLine,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing8,
bottom = TangemTheme.dimens.spacing10,
)
.weight(1f)
.align(CenterVertically),
)
PasteButton(
isPasteButtonVisible = value.isBlank(),
onClick = onPasteClick,
modifier = Modifier
.align(CenterVertically)
.padding(
start = TangemTheme.dimens.spacing4,
end = TangemTheme.dimens.spacing16,
),
)
}
}
}
}
@Composable
internal fun TextFieldWithPaste(
value: String,
placeholder: TextReference,
label: TextReference,
onValueChange: (String) -> Unit,
onPasteClick: (String) -> Unit,
modifier: Modifier = Modifier,
footer: String? = null,
error: TextReference? = null,
isError: Boolean = false,
) {
val (title, color) = if (isError && error != null) {
error to TangemTheme.colors.text.warning
} else {
label to TangemTheme.colors.text.secondary
}
FooterContainer(modifier, footer) {
Row(
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
) {
Column(
modifier = Modifier
.weight(1f)
.padding(TangemTheme.dimens.spacing12),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.body2,
color = color,
)
SimpleTextField(
value = value,
placeholder = placeholder,
onValueChange = onValueChange,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing6),
)
}
PasteButton(
isPasteButtonVisible = value.isBlank(),
onClick = onPasteClick,
modifier = Modifier
.align(CenterVertically)
.padding(end = TangemTheme.dimens.spacing16),
)
}
}
}
@Composable
internal fun TextFieldWithInfo(
value: String,
label: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
info: TextReference? = null,
footer: String? = null,
isSingleLine: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
) {
FooterContainer(
footer = footer,
footerTopPadding = TangemTheme.dimens.spacing6,
modifier = modifier,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
top = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing14,
),
) {
Text(
text = label,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
Row {
SimpleTextField(
value = value,
onValueChange = onValueChange,
visualTransformation = visualTransformation,
singleLine = isSingleLine,
keyboardOptions = keyboardOptions,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing6)
.weight(1f),
)
info?.let {
Text(
text = it.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8)
.align(Alignment.Bottom),
)
}
}
}
}
}
@Composable
private fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) {
val clipboardManager = LocalClipboardManager.current
val hapticFeedback = LocalHapticFeedback.current
if (isPasteButtonVisible) {
Box(modifier = modifier) {
Text(
text = "Paste",
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary2,
modifier = Modifier
.background(
color = TangemTheme.colors.button.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(
horizontal = TangemTheme.dimens.spacing10,
vertical = TangemTheme.dimens.spacing2,
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(radius = TangemTheme.dimens.radius8),
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onClick(
clipboardManager
.getText()
?.toString()
.orEmpty(),
)
},
),
)
}
} else {
Icon(
painter = painterResource(id = R.drawable.ic_close_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = stringResource(R.string.common_close),
modifier = modifier
.size(TangemTheme.dimens.size20)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(radius = TangemTheme.dimens.radius10),
onClick = { onClick("") },
),
)
}
}
@Composable
private fun SimpleTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
placeholder: TextReference? = null,
singleLine: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
) {
val focusRequester = remember { FocusRequester() }
BasicTextField(
value = value,
onValueChange = onValueChange,
textStyle = TangemTheme.typography.body2.copy(color = TangemTheme.colors.text.primary1),
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
singleLine = singleLine,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
decorationBox = { textValue ->
Box {
if (value.isBlank() && placeholder != null) {
Text(
text = placeholder.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.disabled,
modifier = Modifier,
)
}
textValue()
}
},
modifier = modifier
.focusRequester(focusRequester),
)
}
//region preview
@Preview
@Composable
private fun TextFieldPreview_Light() {
TangemTheme {
Column {
TextFieldWithPaste(
value = "",
label = TextReference.Res(R.string.send_recipient),
placeholder = TextReference.Res(R.string.send_enter_address_field),
onValueChange = {},
onPasteClick = {},
)
SpacerH8()
TextFieldWithPasteAndIcon(
value = "",
label = TextReference.Res(R.string.send_extras_hint_memo),
placeholder = TextReference.Res(R.string.send_optional_field),
onValueChange = {},
onPasteClick = {},
)
SpacerH8()
TextFieldWithInfo(
value = "Text",
label = stringResource(R.string.send_extras_hint_memo),
info = TextReference.Res(R.string.send_optional_field),
footer = stringResource(R.string.send_max_fee),
onValueChange = {},
)
}
}
}
@Preview
@Composable
private fun TextFieldPreview_Dark() {
TangemTheme(isDark = true) {
Column {
TextFieldWithPaste(
value = "",
label = TextReference.Res(R.string.send_recipient),
placeholder = TextReference.Res(R.string.send_enter_address_field),
onValueChange = {},
onPasteClick = {},
)
SpacerH8()
TextFieldWithPasteAndIcon(
value = "",
label = TextReference.Res(R.string.send_extras_hint_memo),
placeholder = TextReference.Res(R.string.send_optional_field),
onValueChange = {},
onPasteClick = {},
)
SpacerH8()
TextFieldWithInfo(
value = "Text",
label = stringResource(R.string.send_extras_hint_memo),
info = TextReference.Res(R.string.send_optional_field),
footer = stringResource(R.string.send_max_fee),
onValueChange = {},
)
}
}
}
//endregion

View file

@ -0,0 +1,193 @@
package com.tangem.features.send.impl.presentation.ui.send
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.toBigDecimalOrDefault
import com.tangem.core.ui.components.inputrow.InputRowDefault
import com.tangem.core.ui.components.inputrow.InputRowImage
import com.tangem.core.ui.components.inputrow.InputRowRecipientDefault
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.FeeType
@Suppress("LongMethod")
@Composable
internal fun SendContent(uiState: SendUiState) {
val amountState = uiState.amountState ?: return
val recipientState = uiState.recipientState ?: return
val feeState = uiState.feeState ?: return
val sendState = uiState.sendState ?: return
val isSuccess = sendState.isSuccess.collectAsStateWithLifecycle()
val timestamp = sendState.transactionDate.collectAsStateWithLifecycle()
Column(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
AnimatedVisibility(visible = isSuccess.value) {
TransactionDoneTitle(
titleRes = R.string.sent_transaction_sent_title,
date = timestamp.value,
)
}
AnimatedVisibility(visible = !isSuccess.value) {
FromWallet(
walletName = amountState.walletName,
walletBalance = amountState.walletBalance,
)
}
AmountBlock(
amountState = amountState,
isSuccess = isSuccess,
onClick = uiState.clickIntents::showAmount,
)
RecipientBlock(
recipientState = recipientState,
isSuccess = isSuccess,
onClick = uiState.clickIntents::showRecipient,
)
FeeBlock(
feeState = feeState,
isSuccess = isSuccess,
onClick = uiState.clickIntents::showFee,
)
}
}
@Composable
private fun FromWallet(walletName: String, walletBalance: String) {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.button.disabled)
.padding(TangemTheme.dimens.spacing12),
) {
Text(
text = buildAnnotatedString {
append(stringResource(R.string.send_from_wallet_android))
append(" ")
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(walletName)
}
},
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
)
Text(
text = walletBalance,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing8,
),
)
}
}
@Composable
private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: State<Boolean>, onClick: () -> Unit) {
val amount = amountState.amountTextField.collectAsStateWithLifecycle()
val cryptoAmount = formatCryptoAmount(
cryptoCurrency = amountState.cryptoCurrencyStatus.currency,
cryptoAmount = amount.value.value.toBigDecimalOrDefault(),
)
val fiatAmount = BigDecimalFormatter.formatFiatAmount(
fiatAmount = amount.value.fiatValue.toBigDecimalOrDefault(),
fiatCurrencyCode = amountState.appCurrency.code,
fiatCurrencySymbol = amountState.appCurrency.symbol,
)
InputRowImage(
title = TextReference.Res(R.string.send_amount_label),
subtitle = TextReference.Str(cryptoAmount),
caption = TextReference.Str(fiatAmount),
tokenIconState = amountState.tokenIconState,
showNetworkIcon = true,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(enabled = !isSuccess.value) { onClick() },
)
}
@Composable
private fun RecipientBlock(recipientState: SendStates.RecipientState, isSuccess: State<Boolean>, onClick: () -> Unit) {
val address = recipientState.addressTextField.collectAsStateWithLifecycle()
val memo = recipientState.memoTextField?.collectAsStateWithLifecycle()
Column(
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(enabled = !isSuccess.value) { onClick() },
) {
val showMemo = memo != null && memo.value.value.isNotBlank()
InputRowRecipientDefault(
title = TextReference.Res(R.string.send_recipient),
value = address.value.value,
showDivider = showMemo,
)
if (showMemo) {
InputRowDefault(
title = TextReference.Res(R.string.send_extras_hint_memo),
text = TextReference.Str(memo?.value?.value.orEmpty()),
)
}
}
}
@Composable
private fun FeeBlock(feeState: SendStates.FeeState, isSuccess: State<Boolean>, onClick: () -> Unit) {
val feeSelector =
feeState.feeSelectorState.collectAsStateWithLifecycle().value as? FeeSelectorState.Content ?: return
val customValue = feeSelector.customValues.collectAsStateWithLifecycle().value.getOrNull(0)
val feeValue = formatCryptoAmount(
cryptoCurrency = feeState.cryptoCurrencyStatus.currency,
cryptoAmount = when (val selectedFee = feeSelector.fees) {
is TransactionFee.Single -> selectedFee.normal.amount.value
is TransactionFee.Choosable -> when (feeSelector.selectedFee) {
FeeType.SLOW -> selectedFee.minimum.amount.value
FeeType.MARKET -> selectedFee.normal.amount.value
FeeType.FAST -> selectedFee.priority.amount.value
FeeType.CUSTOM -> customValue?.value.toBigDecimalOrDefault()
}
},
)
InputRowDefault(
title = TextReference.Res(R.string.send_network_fee_title),
text = TextReference.Str(feeValue),
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(enabled = !isSuccess.value) { onClick() },
)
}

View file

@ -7,6 +7,7 @@ import java.math.BigInteger
internal fun validateMemo(memo: String, cryptoCurrency: CryptoCurrency?): Boolean {
if (cryptoCurrency == null) return false
if (memo.isEmpty()) return true
return when (cryptoCurrency.network.id.value) {
Blockchain.XRP.id -> {
val tag = memo.toLongOrNull()

View file

@ -4,6 +4,8 @@ import com.tangem.features.send.impl.presentation.state.fee.FeeType
interface SendClickIntents {
fun popBackStack()
fun onBackClick()
fun onNextClick()
@ -33,4 +35,16 @@ interface SendClickIntents {
fun onSubtractSelect(value: Boolean)
// endregion
// region Send
fun onSendClick()
fun showAmount()
fun showRecipient()
fun showFee()
fun onExploreClick(txUrl: String)
// endregion
}

View file

@ -9,6 +9,7 @@ import arrow.core.getOrElse
import com.tangem.blockchain.blockchains.xrp.XrpAddressService
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.address.Address
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.Provider
import com.tangem.core.ui.utils.BigDecimalFormatter
@ -18,8 +19,11 @@ import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.utils.convertToAmount
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.txhistory.models.TxHistoryItem
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.walletmanager.WalletManagersFacade
@ -28,6 +32,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.navigation.InnerSendRouter
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
import com.tangem.features.send.impl.presentation.state.SendStateFactory
import com.tangem.features.send.impl.presentation.state.SendUiState
@ -48,7 +53,7 @@ import java.math.BigDecimal
import javax.inject.Inject
import kotlin.properties.Delegates
@Suppress("LongParameterList")
@Suppress("LongParameterList", "TooManyFunctions", "LargeClass")
@HiltViewModel
internal class SendViewModel @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
@ -60,6 +65,8 @@ internal class SendViewModel @Inject constructor(
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val sendTransactionUseCase: SendTransactionUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val walletManagersFacade: WalletManagersFacade,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, SendClickIntents {
@ -73,7 +80,8 @@ internal class SendViewModel @Inject constructor(
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
private var innerRouter: StateRouter by Delegates.notNull()
private var inneRrouter: InnerSendRouter by Delegates.notNull()
private var stateRouter: StateRouter by Delegates.notNull()
private val stateFactory = SendStateFactory(
clickIntents = this,
@ -102,9 +110,10 @@ internal class SendViewModel @Inject constructor(
getFee()
}
fun setRouter(router: StateRouter) {
innerRouter = router
uiState = uiState.copy(currentState = router.currentState)
fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) {
inneRrouter = router
this.stateRouter = stateRouter
uiState = uiState.copy(currentState = stateRouter.currentState)
}
private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) {
@ -271,9 +280,10 @@ internal class SendViewModel @Inject constructor(
}
// region screen state navigation
override fun onBackClick() = innerRouter.onBackClick()
override fun onNextClick() = innerRouter.onNextClick()
override fun onPrevClick() = innerRouter.onPrevClick()
override fun popBackStack() = stateRouter.popBackStack()
override fun onBackClick() = stateRouter.onBackClick()
override fun onNextClick() = stateRouter.onNextClick()
override fun onPrevClick() = stateRouter.onPrevClick()
override fun onQrCodeScanClick() {
// TODO Add QR code scanning
@ -314,7 +324,7 @@ internal class SendViewModel @Inject constructor(
}
private fun checkIfXrpAddressValue(value: String): Boolean {
if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.first() == XRP_X_ADDRESS) {
if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.firstOrNull() == XRP_X_ADDRESS) {
viewModelScope.launch(dispatchers.io) {
val result = XrpAddressService.decodeXAddress(value)
onRecipientAddressValueChange(result?.address.orEmpty())
@ -382,8 +392,108 @@ internal class SendViewModel @Inject constructor(
}
//endregion
// region send state clicks
override fun onSendClick() {
val sendState = uiState.sendState ?: return
if (sendState.isSuccess.value) popBackStack()
sendState.isSending.update { true }
viewModelScope.launch(dispatchers.io) {
verifyAndSendTransaction()
}
}
private suspend fun verifyAndSendTransaction() {
val sendState = uiState.sendState ?: return
val amount = uiState.amountState?.amountTextField?.value ?: return
val recipient = uiState.recipientState?.addressTextField?.value ?: return
val feeState = uiState.feeState?.feeSelectorState?.value as? FeeSelectorState.Content ?: return
val memo = uiState.recipientState?.memoTextField?.value
val fee = getFee(feeState) ?: return
val amountToSend = amount.value.toBigDecimal().convertToAmount(cryptoCurrency)
// todo add notifications [[REDACTED_JIRA]]
// val transactionErrors = walletManagersFacade.validateTransaction(
// amount = amountToSend,
// fee = fee.amount,
// userWalletId = userWalletId,
// network = cryptoCurrency.network,
// )
val txData = walletManagersFacade.createTransaction(
amount = amountToSend,
fee = fee,
memo = memo?.value,
destination = recipient.value,
userWalletId = userWalletId,
network = cryptoCurrency.network,
) ?: return
sendTransactionUseCase(
txData = txData,
userWallet = userWallet,
network = cryptoCurrency.network,
).fold(
ifLeft = {
sendState.isSending.update { false }
// todo add notifications [[REDACTED_JIRA]]
},
ifRight = {
sendState.transactionDate.update {
txData.date?.timeInMillis ?: System.currentTimeMillis()
}
sendState.isSuccess.update { true }
sendState.txUrl.update {
getTxUrl(txData.hash.orEmpty())
}
},
)
}
private fun getFee(feeState: FeeSelectorState.Content): Fee? {
return when (val selectedFee = feeState.fees) {
is TransactionFee.Choosable -> {
when (feeState.selectedFee) {
FeeType.SLOW -> selectedFee.minimum
FeeType.MARKET -> selectedFee.normal
FeeType.FAST -> selectedFee.priority
FeeType.CUSTOM -> {
val feeAmount = feeState.customValues.value.firstOrNull()?.value
?.let { BigDecimal(it) } ?: return null
Fee.Common(feeAmount.convertToAmount(cryptoCurrency))
}
}
}
is TransactionFee.Single -> selectedFee.normal
}
}
override fun showAmount() = stateRouter.showAmount(isFromSend = true)
override fun showRecipient() = stateRouter.showRecipient(isFromSend = true)
override fun showFee() = stateRouter.showFee(isFromSend = true)
override fun onExploreClick(txUrl: String) = inneRrouter.openUrl(txUrl)
private fun getTxUrl(hash: String): String {
val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value)
// TODO: Fix ton tx urls [REDACTED_TASK_KEY]
return if (blockchain == Blockchain.TON || blockchain == Blockchain.TONTestnet) {
EMPTY
} else {
getExplorerTransactionUrlUseCase(
txHash = hash,
networkId = cryptoCurrency.network.id,
)
}
}
// endregion
companion object {
private const val XRP_X_ADDRESS = 'X'
private const val DEFAULT_VALUE = "0.00"
private const val EMPTY = ""
}
}