Updated on 2026-08-14
This commit is contained in:
parent
e1fd5fc2d2
commit
cd28b90873
15 changed files with 557 additions and 78 deletions
|
|
@ -14,7 +14,11 @@ dependencies {
|
|||
/** AndroidX */
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
implementation(deps.androidx.appCompat)
|
||||
|
||||
/** Other dependencies */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.material)
|
||||
implementation(deps.arrow.core)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
|
|
@ -23,13 +27,23 @@ dependencies {
|
|||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.navigation)
|
||||
implementation(deps.compose.navigation.hilt)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.featuretoggles)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.appCurrency)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
|
||||
/** Feature modules */
|
||||
implementation(projects.features.send.api)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.features.send.impl.presentation.send.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
/**
|
||||
* Segmented buttons config
|
||||
*
|
||||
* @param title button title
|
||||
* @param iconState currency icon state
|
||||
* @param iconUrl currency icon url
|
||||
* @param isFiat is fiat currency
|
||||
*/
|
||||
@Immutable
|
||||
internal data class SendAmountSegmentedButtonsConfig(
|
||||
val title: TextReference,
|
||||
val iconState: TokenIconState? = null,
|
||||
val iconUrl: String? = null,
|
||||
val isFiat: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.tangem.features.send.impl.presentation.send.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.send.state.fields.SendTextField
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
||||
/**
|
||||
* Ui states of the send screen
|
||||
*/
|
||||
@Immutable
|
||||
internal sealed class SendUiState {
|
||||
|
||||
/** States with content */
|
||||
sealed class Content : SendUiState() {
|
||||
|
||||
abstract val nextButtonEnabled: Boolean
|
||||
|
||||
/** Initial state */
|
||||
data class Initial(
|
||||
override val nextButtonEnabled: Boolean = false,
|
||||
) : Content()
|
||||
|
||||
/** Amount state */
|
||||
data class AmountState(
|
||||
override val nextButtonEnabled: Boolean = false,
|
||||
val walletName: String,
|
||||
val walletBalance: String,
|
||||
val tokenIconState: TokenIconState,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val appCurrency: AppCurrency,
|
||||
val isFiatValue: Boolean,
|
||||
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonConfig>,
|
||||
val amountTextField: SendTextField.Amount,
|
||||
) : Content()
|
||||
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Recipient state */
|
||||
data class RecipientState(
|
||||
override val nextButtonEnabled: Boolean = false,
|
||||
) : Content()
|
||||
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Fee and speed state */
|
||||
data class FeeState(
|
||||
override val nextButtonEnabled: Boolean = false,
|
||||
) : Content()
|
||||
|
||||
// todo [REDACTED_JIRA]
|
||||
/** Send state */
|
||||
data class SendState(
|
||||
override val nextButtonEnabled: Boolean = true,
|
||||
) : Content()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.features.send.impl.presentation.send.state.fields
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal sealed class SendTextField {
|
||||
|
||||
/** Current value */
|
||||
abstract val value: String
|
||||
|
||||
/** Lambda be invoked when value is been changed */
|
||||
abstract val onValueChange: (String) -> Unit
|
||||
|
||||
/** Keyboard options */
|
||||
abstract val keyboardOptions: KeyboardOptions
|
||||
|
||||
/** Label */
|
||||
abstract val label: TextReference
|
||||
|
||||
/** Placeholder (hint) */
|
||||
abstract val placeholder: TextReference
|
||||
|
||||
data class Amount(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
override val label: TextReference,
|
||||
override val placeholder: TextReference,
|
||||
val fiatValue: String,
|
||||
val isError: Boolean,
|
||||
val error: TextReference,
|
||||
) : SendTextField()
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package com.tangem.features.send.impl.presentation.send.ui
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
|
||||
data class SendBottomSheetConfig(
|
||||
val currentState: SendStates,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -18,17 +18,18 @@ 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.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.send.state.SendUiState
|
||||
|
||||
@Composable
|
||||
internal fun SendNavigationButtons(currentState: SendStates) {
|
||||
internal fun SendNavigationButtons(uiState: SendUiState.Content) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
SendSecondaryNavigationButton(currentState)
|
||||
SendSecondaryNavigationButton(uiState)
|
||||
SendPrimaryNavigationButton(
|
||||
currentState,
|
||||
uiState = uiState,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
|
|
@ -37,9 +38,9 @@ internal fun SendNavigationButtons(currentState: SendStates) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun SendSecondaryNavigationButton(currentState: SendStates) {
|
||||
private fun SendSecondaryNavigationButton(uiState: SendUiState.Content) {
|
||||
AnimatedVisibility(
|
||||
visible = currentState == SendStates.Recipient || currentState == SendStates.Fee,
|
||||
visible = uiState is SendUiState.Content.RecipientState || uiState is SendUiState.Content.FeeState,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
|
|
@ -57,40 +58,37 @@ private fun SendSecondaryNavigationButton(currentState: SendStates) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun SendPrimaryNavigationButton(currentState: SendStates, modifier: Modifier = Modifier) {
|
||||
val buttonTextId = when (currentState) {
|
||||
SendStates.Amount,
|
||||
SendStates.Recipient,
|
||||
SendStates.Fee,
|
||||
private fun SendPrimaryNavigationButton(uiState: SendUiState.Content, modifier: Modifier = Modifier) {
|
||||
val buttonTextId = when (uiState) {
|
||||
is SendUiState.Content.AmountState,
|
||||
is SendUiState.Content.RecipientState,
|
||||
is SendUiState.Content.FeeState,
|
||||
-> R.string.common_next
|
||||
SendStates.Filled -> R.string.common_send
|
||||
SendStates.Done -> R.string.common_close
|
||||
is SendUiState.Content.SendState -> R.string.common_send
|
||||
else -> R.string.common_close
|
||||
}
|
||||
AnimatedContent(
|
||||
targetState = buttonTextId,
|
||||
label = "Update send screen state",
|
||||
modifier = modifier,
|
||||
) { textId ->
|
||||
when (currentState) {
|
||||
SendStates.Amount,
|
||||
SendStates.Recipient,
|
||||
SendStates.Fee,
|
||||
SendStates.Done,
|
||||
-> PrimaryButton(
|
||||
if (uiState is SendUiState.Content.SendState) {
|
||||
PrimaryButtonIconEnd(
|
||||
text = stringResource(textId),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
enabled = uiState.nextButtonEnabled,
|
||||
onClick = {
|
||||
// todo add next click
|
||||
},
|
||||
)
|
||||
} else {
|
||||
PrimaryButton(
|
||||
text = stringResource(textId),
|
||||
enabled = uiState.nextButtonEnabled,
|
||||
onClick = {
|
||||
// todo add next click
|
||||
},
|
||||
)
|
||||
SendStates.Filled -> {
|
||||
PrimaryButtonIconEnd(
|
||||
text = stringResource(textId),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
onClick = {
|
||||
// todo add next click
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,17 +9,14 @@ import androidx.compose.foundation.layout.imePadding
|
|||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetDraggableHeader
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.send.state.SendUiState
|
||||
|
||||
@Composable
|
||||
fun SendScreen() {
|
||||
// todo will be removed
|
||||
val config = remember { SendBottomSheetConfig(SendStates.Amount) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
|
|
@ -42,7 +39,7 @@ fun SendScreen() {
|
|||
) {
|
||||
SendScreenContent()
|
||||
}
|
||||
SendNavigationButtons(config.currentState)
|
||||
SendNavigationButtons(uiState = SendUiState.Content.Initial())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
package com.tangem.features.send.impl.presentation.send.ui
|
||||
|
||||
/**
|
||||
* Screen states of Send
|
||||
*/
|
||||
enum class SendStates {
|
||||
Amount,
|
||||
Recipient,
|
||||
Fee,
|
||||
Filled,
|
||||
Done,
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
/** Get next [SendStates] */
|
||||
fun SendStates.next(): SendStates {
|
||||
return if (this.ordinal < SendStates.values().last().ordinal) {
|
||||
SendStates.values()[this.ordinal + 1]
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
/** Get previous [SendStates] */
|
||||
fun SendStates.previous(): SendStates {
|
||||
return if (this.ordinal > 0) {
|
||||
SendStates.values()[this.ordinal - 1]
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
package com.tangem.features.send.impl.presentation.send.ui.amount
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
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.BottomCenter
|
||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.input.OffsetMapping
|
||||
import androidx.compose.ui.text.input.TransformedText
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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.send.state.fields.SendTextField
|
||||
|
||||
@Composable
|
||||
internal fun ColumnScope.AmountField(
|
||||
sendField: SendTextField.Amount,
|
||||
cryptoSymbol: String,
|
||||
fiatSymbol: String,
|
||||
isFiat: Boolean,
|
||||
) {
|
||||
val value = if (isFiat) sendField.fiatValue else sendField.value
|
||||
val secondaryValue = if (!isFiat) sendField.fiatValue else sendField.value
|
||||
val symbol = if (isFiat) fiatSymbol else cryptoSymbol
|
||||
val secondarySymbol = if (!isFiat) fiatSymbol else cryptoSymbol
|
||||
|
||||
AmountFieldInner(
|
||||
value = value,
|
||||
placeholder = sendField.placeholder,
|
||||
symbol = symbol,
|
||||
onValueChange = sendField.onValueChange,
|
||||
keyboardOptions = sendField.keyboardOptions,
|
||||
modifier = Modifier
|
||||
.align(CenterHorizontally)
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing24,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(CenterHorizontally)
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing8,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = "$secondaryValue $secondarySymbol",
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.align(BottomCenter)
|
||||
.padding(bottom = TangemTheme.dimens.spacing32),
|
||||
)
|
||||
AmountFieldError(
|
||||
isError = sendField.isError,
|
||||
error = sendField.error,
|
||||
modifier = Modifier
|
||||
.align(BottomCenter)
|
||||
.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountFieldInner(
|
||||
value: String,
|
||||
placeholder: TextReference,
|
||||
symbol: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
keyboardOptions: KeyboardOptions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
BasicTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = modifier
|
||||
.focusRequester(focusRequester)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
textStyle = TangemTheme.typography.h2.copy(
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
keyboardOptions = keyboardOptions,
|
||||
singleLine = true,
|
||||
visualTransformation = AmountVisualTransformation(symbol),
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (value.isBlank()) {
|
||||
Text(
|
||||
text = "${placeholder.resolveReference()} $symbol",
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter),
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountFieldError(isError: Boolean, error: TextReference, modifier: Modifier = Modifier) {
|
||||
AnimatedVisibility(
|
||||
visible = isError,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = error.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.warning,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class AmountVisualTransformation(
|
||||
private val symbol: String,
|
||||
) : VisualTransformation {
|
||||
override fun filter(text: AnnotatedString): TransformedText {
|
||||
return TransformedText(
|
||||
buildAnnotatedString {
|
||||
append(text)
|
||||
if (text.isNotBlank()) {
|
||||
append(" ")
|
||||
append(symbol)
|
||||
}
|
||||
},
|
||||
object : OffsetMapping {
|
||||
override fun originalToTransformed(offset: Int): Int {
|
||||
return text.length
|
||||
}
|
||||
|
||||
override fun transformedToOriginal(offset: Int): Int {
|
||||
return text.length
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.features.send.impl.presentation.send.ui.amount
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.send.state.SendUiState
|
||||
|
||||
@Composable
|
||||
internal fun AmountFieldContainer(amountState: SendUiState.Content.AmountState, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing4,
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
Text(
|
||||
text = amountState.walletName,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing14)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
Text(
|
||||
text = amountState.walletBalance,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing2)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
TokenIcon(
|
||||
state = amountState.tokenIconState,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing32)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
AmountField(
|
||||
sendField = amountState.amountTextField,
|
||||
cryptoSymbol = amountState.cryptoCurrencyStatus.currency.symbol,
|
||||
fiatSymbol = amountState.appCurrency.symbol,
|
||||
isFiat = amountState.isFiatValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue