Updated on 2026-08-14
This commit is contained in:
parent
33406d0ac1
commit
2a9a80414d
39 changed files with 1074 additions and 499 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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() },
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 = ""
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue