diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt
index 6e5f3a779f..40b4c0c73c 100644
--- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt
@@ -366,7 +366,10 @@ class TradeCryptoMiddleware {
)
}
}
- val bundle = bundleOf(SendRouter.CRYPTO_CURRENCY_KEY to currency)
+ val bundle = bundleOf(
+ SendRouter.CRYPTO_CURRENCY_KEY to currency,
+ SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue,
+ )
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
}
}
@@ -416,7 +419,10 @@ class TradeCryptoMiddleware {
is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token")
}
- val bundle = bundleOf(SendRouter.CRYPTO_CURRENCY_KEY to currency)
+ val bundle = bundleOf(
+ SendRouter.CRYPTO_CURRENCY_KEY to currency,
+ SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue,
+ )
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
}
}
diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml
index 01de6ab65d..a19b13376c 100644
--- a/core/res/src/main/res/values-ru/strings.xml
+++ b/core/res/src/main/res/values-ru/strings.xml
@@ -90,6 +90,7 @@
Нравится
Заблокирован
Основная сеть
+ Далее
Нет
Нет адреса
Нет данных
diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index 4e05bc5ea2..0bb572349d 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -89,6 +89,7 @@
Like
Locked
Main network
+ Next
No
No address
No data
@@ -114,7 +115,6 @@
Submit
Success
Swap
- Next
terms and conditions
Transaction failed
Transactions
@@ -391,6 +391,7 @@
Normal
Priority
Maximum amount
+ Max
Network fee
Sending %s
Total
@@ -399,6 +400,7 @@
%s will be sent
Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while
Invalid address
+ Insufficient funds for transfer
Buy now
I have a promo code…
Tangem Wallet
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
index 3a09e91308..8cf16b6848 100644
--- a/core/ui/build.gradle.kts
+++ b/core/ui/build.gradle.kts
@@ -27,6 +27,7 @@ dependencies {
implementation(deps.compose.material3)
implementation(deps.compose.paging)
implementation(deps.compose.ui.tooling)
+ implementation(deps.compose.coil)
/** Other libraries */
implementation(deps.compose.accompanist.systemUiController)
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt
index 882c791eab..952e17a142 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt
@@ -7,11 +7,10 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
-import com.tangem.core.ui.components.buttons.common.TangemButton
-import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
-import com.tangem.core.ui.components.buttons.common.TangemButtonSize
+import com.tangem.core.ui.components.buttons.common.*
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.res.TangemTheme
@@ -172,6 +171,8 @@ fun SecondaryButton(
modifier: Modifier = Modifier,
showProgress: Boolean = false,
enabled: Boolean = true,
+ size: TangemButtonSize = TangemButtonSize.Default,
+ shape: Shape = size.toShape(),
) {
TangemButton(
modifier = modifier,
@@ -181,6 +182,8 @@ fun SecondaryButton(
colors = TangemButtonsDefaults.secondaryButtonColors,
enabled = enabled,
showProgress = showProgress,
+ size = size,
+ shape = shape,
)
}
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt
index 2d0d4b0b68..eda1d7fcac 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt
@@ -6,6 +6,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
+import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
@@ -30,13 +31,14 @@ fun TangemButton(
size: TangemButtonSize = TangemButtonSize.Default,
elevation: ButtonElevation = TangemButtonsDefaults.elevation,
textStyle: TextStyle = TangemTheme.typography.button,
+ shape: Shape = size.toShape(),
) {
Button(
modifier = modifier.heightIn(min = size.toHeightDp()),
onClick = { if (!showProgress) onClick() },
enabled = enabled,
elevation = elevation,
- shape = size.toShape(),
+ shape = shape,
colors = colors,
contentPadding = size.toContentPadding(icon = icon),
) {
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt
new file mode 100644
index 0000000000..df18f78ab0
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt
@@ -0,0 +1,161 @@
+package com.tangem.core.ui.components.buttons.segmentedbutton
+
+import androidx.compose.foundation.LocalIndication
+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.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
+import com.tangem.core.ui.res.TangemTheme
+import kotlinx.collections.immutable.PersistentList
+import kotlinx.collections.immutable.persistentListOf
+
+/**
+ * Segmented buttons
+ *
+ * [Figma component](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=1961-2004&mode=design&t=OFPQ18YhLHVAANab-4)
+ *
+ * @param config list of buttons in SegmentedButtons
+ * @param onClick button click
+ * @param modifier component modifier
+ * @param color default button color
+ * @param selectedColor selected button color
+ * @param dividerColor border and divider color
+ * @param showIndication show ripple indication
+ * @param buttonContent content as separate button
+ */
+@Composable
+inline fun SegmentedButtons(
+ config: PersistentList,
+ crossinline onClick: (T) -> Unit,
+ modifier: Modifier = Modifier,
+ color: Color = TangemTheme.colors.background.tertiary,
+ selectedColor: Color = TangemTheme.colors.background.action,
+ dividerColor: Color = TangemTheme.colors.stroke.primary,
+ showIndication: Boolean = true,
+ crossinline buttonContent: @Composable (T) -> Unit,
+) {
+ if (config.isEmpty() || config.size == 1) return
+
+ var selected by remember { mutableIntStateOf(0) }
+
+ Row(
+ modifier = modifier
+ .clip(RoundedCornerShape(TangemTheme.dimens.radius26))
+ .background(dividerColor)
+ .padding(TangemTheme.dimens.spacing1),
+ horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing1),
+ ) {
+ repeat(config.size) { index ->
+ val leftRadius = if (index == 0) TangemTheme.dimens.radius26 else TangemTheme.dimens.radius0
+ val rightRadius = if (index == config.lastIndex) TangemTheme.dimens.radius26 else TangemTheme.dimens.radius0
+
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .background(
+ color = if (index == selected) selectedColor else color,
+ shape = RoundedCornerShape(
+ topStart = leftRadius,
+ topEnd = rightRadius,
+ bottomEnd = rightRadius,
+ bottomStart = leftRadius,
+ ),
+ )
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = if (showIndication) LocalIndication.current else null,
+ ) {
+ onClick(config[index])
+ selected = index
+ },
+ ) {
+ buttonContent.invoke(config[index])
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun SegmentedButtonsPreview_Light(
+ @PreviewParameter(SegmentedButtonsPreviewProvider::class) config: PersistentList,
+) {
+ TangemTheme {
+ SegmentedButtons(
+ config = config,
+ onClick = {},
+ ) {
+ Text(
+ text = it.text,
+ modifier = Modifier.padding(TangemTheme.dimens.spacing16),
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun SegmentedButtonsPreview_Dark(
+ @PreviewParameter(SegmentedButtonsPreviewProvider::class) config: PersistentList,
+) {
+ TangemTheme(isDark = true) {
+ SegmentedButtons(
+ config = config,
+ onClick = {},
+ ) {
+ Text(
+ text = it.text,
+ modifier = Modifier.padding(TangemTheme.dimens.spacing16),
+ )
+ }
+ }
+}
+
+//region Preview config
+/**
+ * Segmented buttons preview model
+ *
+ * @param text button title
+ */
+internal data class SegmentedButtonsConfigPreview(
+ val text: String,
+)
+
+/**
+ * Segmented button preview provider
+ */
+internal class SegmentedButtonsPreviewProvider :
+ CollectionPreviewParameterProvider>(
+ collection = listOf(
+ persistentListOf(
+ SegmentedButtonsConfigPreview(
+ text = "Title 1",
+ ),
+ SegmentedButtonsConfigPreview(
+ text = "Title 2",
+ ),
+ SegmentedButtonsConfigPreview(
+ text = "Title 3",
+ ),
+ ),
+ persistentListOf(
+ SegmentedButtonsConfigPreview(
+ text = "Title 1",
+ ),
+ SegmentedButtonsConfigPreview(
+ text = "Title 2",
+ ),
+ ),
+ ),
+ )
+
+//endregion
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt
new file mode 100644
index 0000000000..792cbab8b0
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt
@@ -0,0 +1,60 @@
+package com.tangem.core.ui.components.currency
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import coil.compose.SubcomposeAsyncImage
+import coil.request.ImageRequest
+import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
+import kotlinx.coroutines.launch
+
+@Composable
+internal inline fun DefaultCurrencyIcon(
+ iconData: Any,
+ alpha: Float,
+ colorFilter: ColorFilter?,
+ crossinline errorIcon: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
+ val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
+ val isDarkTheme = isSystemInDarkTheme()
+ val coroutineScope = rememberCoroutineScope()
+
+ SubcomposeAsyncImage(
+ modifier = modifier
+ .background(
+ color = iconBackgroundColor,
+ shape = TangemTheme.shapes.roundedCorners8,
+ ),
+ model = ImageRequest.Builder(context = LocalContext.current)
+ .data(iconData)
+ .crossfade(enable = true)
+ .allowHardware(false)
+ .listener(
+ onSuccess = { _, result ->
+ if (isDarkTheme) {
+ coroutineScope.launch {
+ val color = ImageBackgroundContrastChecker(
+ drawable = result.drawable,
+ backgroundColor = itemBackgroundColor,
+ ).getContrastColorIfNeeded(isDarkTheme)
+ iconBackgroundColor = color
+ }
+ }
+ },
+ ).build(),
+ loading = { LoadingIcon() },
+ error = { errorIcon() },
+ alpha = alpha,
+ colorFilter = colorFilter,
+ contentDescription = null,
+ )
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt
new file mode 100644
index 0000000000..879ea4575a
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt
@@ -0,0 +1,38 @@
+package com.tangem.core.ui.components.currency.fiaticon
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.Image
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import com.tangem.core.ui.R
+import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
+
+/**
+ * Simple icon from network
+ *
+ * @param url link to icon
+ * @param fallbackResId fallback icon
+ * @param modifier component modifier
+ */
+@Composable
+fun FiatIcon(
+ url: String?,
+ modifier: Modifier = Modifier,
+ @DrawableRes fallbackResId: Int = R.drawable.ic_shape_circle,
+) {
+ val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url
+
+ DefaultCurrencyIcon(
+ modifier = modifier,
+ iconData = iconData,
+ errorIcon = {
+ Image(
+ painter = painterResource(id = fallbackResId),
+ contentDescription = null,
+ )
+ },
+ alpha = 1f,
+ colorFilter = null,
+ )
+}
\ No newline at end of file
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt
similarity index 55%
rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt
rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt
index dd1be11f5a..dd3e892301 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt
@@ -1,44 +1,36 @@
-package com.tangem.feature.wallet.presentation.common.component.token.icon
+package com.tangem.core.ui.components.currency.tokenicon
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
-import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
-import coil.compose.SubcomposeAsyncImage
-import coil.request.ImageRequest
-import com.tangem.core.ui.res.TangemTheme
-import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
-import com.tangem.feature.wallet.impl.R
-import com.tangem.feature.wallet.presentation.common.state.TokenItemState
-import kotlinx.coroutines.launch
+import com.tangem.core.ui.R
+import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
@Composable
internal fun ContentIcon(
- icon: TokenItemState.IconState,
+ icon: TokenIconState,
alpha: Float,
colorFilter: ColorFilter?,
modifier: Modifier = Modifier,
) {
when (icon) {
- is TokenItemState.IconState.CoinIcon -> CoinIcon(
+ is TokenIconState.CoinIcon -> CoinIcon(
modifier = modifier,
url = icon.url,
fallbackResId = icon.fallbackResId,
alpha = alpha,
colorFilter = colorFilter,
)
- is TokenItemState.IconState.TokenIcon -> TokenIcon(
+ is TokenIconState.TokenIcon -> TokenIcon(
modifier = modifier,
url = icon.url,
alpha = alpha,
@@ -52,14 +44,14 @@ internal fun ContentIcon(
)
},
)
- is TokenItemState.IconState.CustomTokenIcon -> CustomTokenIcon(
+ is TokenIconState.CustomTokenIcon -> CustomTokenIcon(
modifier = modifier,
tint = icon.tint,
background = icon.background,
alpha = alpha,
)
- TokenItemState.IconState.Loading,
- TokenItemState.IconState.Locked,
+ TokenIconState.Loading,
+ TokenIconState.Locked,
-> Unit
}
}
@@ -128,48 +120,4 @@ private fun CustomTokenIcon(tint: Color, background: Color, alpha: Float, modifi
contentDescription = null,
)
}
-}
-
-@Composable
-private inline fun DefaultCurrencyIcon(
- iconData: Any,
- alpha: Float,
- colorFilter: ColorFilter?,
- crossinline errorIcon: @Composable () -> Unit,
- modifier: Modifier = Modifier,
-) {
- var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
- val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
- val isDarkTheme = isSystemInDarkTheme()
- val coroutineScope = rememberCoroutineScope()
-
- SubcomposeAsyncImage(
- modifier = modifier
- .background(
- color = iconBackgroundColor,
- shape = TangemTheme.shapes.roundedCorners8,
- ),
- model = ImageRequest.Builder(context = LocalContext.current)
- .data(iconData)
- .crossfade(enable = true)
- .allowHardware(false)
- .listener(
- onSuccess = { _, result ->
- if (isDarkTheme) {
- coroutineScope.launch {
- val color = ImageBackgroundContrastChecker(
- drawable = result.drawable,
- backgroundColor = itemBackgroundColor,
- ).getContrastColorIfNeeded(isDarkTheme)
- iconBackgroundColor = color
- }
- }
- },
- ).build(),
- loading = { LoadingIcon() },
- error = { errorIcon() },
- alpha = alpha,
- colorFilter = colorFilter,
- contentDescription = null,
- )
}
\ No newline at end of file
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/IconBadge.kt
similarity index 96%
rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt
rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/IconBadge.kt
index 6fd0339923..e21ce3274c 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/IconBadge.kt
@@ -1,4 +1,4 @@
-package com.tangem.feature.wallet.presentation.common.component.token.icon
+package com.tangem.core.ui.components.currency.tokenicon
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt
similarity index 77%
rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt
rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt
index 275c58d54f..07d299f583 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt
@@ -1,4 +1,4 @@
-package com.tangem.feature.wallet.presentation.common.component.token.icon
+package com.tangem.core.ui.components.currency.tokenicon
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@@ -14,14 +14,22 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.res.TangemTheme
-import com.tangem.feature.wallet.presentation.common.state.TokenItemState.IconState as TokenIconState
private const val GRAY_SCALE_SATURATION = 0f
private const val GRAY_SCALE_ALPHA = 0.4f
private const val NORMAL_ALPHA = 1f
+/**
+ * Cryptocurrency icon with network badge
+ *
+ * TODO [separate domain from ui]([REDACTED_JIRA])
+ *
+ * @param state cryptocurrency icon config
+ * @param modifier component modifier
+ * @param shouldDisplayNetwork specifies whether to display network badge
+ */
@Composable
-internal fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier) {
+fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier, shouldDisplayNetwork: Boolean = true) {
BaseContainer(modifier = modifier) {
val iconModifier = Modifier
.align(Alignment.Center)
@@ -34,7 +42,11 @@ internal fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier) {
is TokenIconState.CustomTokenIcon,
is TokenIconState.TokenIcon,
-> {
- ContentIconContainer(modifier = iconModifier, icon = state)
+ ContentIconContainer(
+ icon = state,
+ modifier = iconModifier,
+ shouldDisplayNetwork = shouldDisplayNetwork,
+ )
}
}
}
@@ -60,7 +72,11 @@ private fun LockedIcon(modifier: Modifier = Modifier) {
}
@Composable
-private fun BoxScope.ContentIconContainer(icon: TokenIconState, modifier: Modifier = Modifier) {
+private fun BoxScope.ContentIconContainer(
+ icon: TokenIconState,
+ modifier: Modifier = Modifier,
+ shouldDisplayNetwork: Boolean = true,
+) {
val networkBadgeOffset = TangemTheme.dimens.spacing4
val (alpha, colorFilter) = remember(icon.isGrayscale) {
if (icon.isGrayscale) {
@@ -77,7 +93,7 @@ private fun BoxScope.ContentIconContainer(icon: TokenIconState, modifier: Modifi
colorFilter = colorFilter,
)
- if (icon.networkBadgeIconResId != null) {
+ if (icon.networkBadgeIconResId != null && shouldDisplayNetwork) {
NetworkBadge(
modifier = Modifier
.offset(x = networkBadgeOffset, y = -networkBadgeOffset)
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt
new file mode 100644
index 0000000000..f51adb19b3
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt
@@ -0,0 +1,85 @@
+package com.tangem.core.ui.components.currency.tokenicon
+
+import androidx.annotation.DrawableRes
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.Color
+
+/**
+ * Represents the various states an icon can be in.
+ *
+ * [REDACTED_TODO_COMMENT]
+ */
+@Immutable
+sealed class TokenIconState {
+
+ abstract val isGrayscale: Boolean
+ abstract val showCustomBadge: Boolean
+ abstract val networkBadgeIconResId: Int?
+
+ /**
+ * Represents a coin icon.
+ *
+ * @property url The URL where the coin icon can be fetched from. May be `null` if not found.
+ * @property fallbackResId The drawable resource ID to be used as a fallback if the URL is not available.
+ * @property isGrayscale Specifies whether to show the icon in grayscale.
+ * @property showCustomBadge Specifies whether to show the custom token badge.
+ */
+ data class CoinIcon(
+ val url: String?,
+ @DrawableRes val fallbackResId: Int,
+ override val isGrayscale: Boolean,
+ override val showCustomBadge: Boolean,
+ ) : TokenIconState() {
+
+ override val networkBadgeIconResId: Int? = null
+ }
+
+ /**
+ * Represents a token icon.
+ *
+ * @property url The URL where the token icon can be fetched from. May be `null` if not found.
+ * @property networkBadgeIconResId The drawable resource ID for the network badge.
+ * @property isGrayscale Specifies whether to show the icon in grayscale.
+ * @property showCustomBadge Specifies whether to show the custom token badge.
+ * @property fallbackTint The color to be used for tinting the fallback icon.
+ * @property fallbackBackground The background color to be used for the fallback icon.
+ */
+ data class TokenIcon(
+ val url: String?,
+ @DrawableRes override val networkBadgeIconResId: Int,
+ override val isGrayscale: Boolean,
+ override val showCustomBadge: Boolean,
+ val fallbackTint: Color,
+ val fallbackBackground: Color,
+ ) : TokenIconState()
+
+ /**
+ * Represents a custom token icon.
+ *
+ * @property tint The color to be used for tinting the icon.
+ * @property background The background color to be used for the icon.
+ * @property networkBadgeIconResId The drawable resource ID for the network badge.
+ * @property isGrayscale Specifies whether to show the icon in grayscale.
+ */
+ data class CustomTokenIcon(
+ val tint: Color,
+ val background: Color,
+ @DrawableRes override val networkBadgeIconResId: Int,
+ override val isGrayscale: Boolean,
+ ) : TokenIconState() {
+
+ override val showCustomBadge: Boolean = true
+ }
+
+ object Loading : TokenIconState() {
+ override val isGrayscale: Boolean = false
+ override val showCustomBadge: Boolean = false
+ override val networkBadgeIconResId: Int? = null
+ }
+
+ object Locked : TokenIconState() {
+ override val isGrayscale: Boolean = false
+ override val showCustomBadge: Boolean = false
+ override val networkBadgeIconResId: Int? = null
+ }
+}
\ No newline at end of file
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt
similarity index 69%
rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt
rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt
index 0af718eb22..6509d4baf1 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt
@@ -1,27 +1,27 @@
-package com.tangem.feature.wallet.presentation.common.utils
+package com.tangem.core.ui.components.currency.tokenicon.converter
+import com.tangem.common.Converter
+import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.extensions.getTintForTokenIcon
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
-import com.tangem.feature.wallet.presentation.common.state.TokenItemState
-import com.tangem.utils.converter.Converter
-internal class CryptoCurrencyToIconStateConverter : Converter {
+/**
+ * Converts [CryptoCurrencyStatus] to [TokenIconState]
+ */
+class CryptoCurrencyToIconStateConverter : Converter {
- override fun convert(value: CryptoCurrencyStatus): TokenItemState.IconState {
+ override fun convert(value: CryptoCurrencyStatus): TokenIconState {
return when (val currency = value.currency) {
is CryptoCurrency.Coin -> getIconStateForCoin(currency, value.value.isError)
is CryptoCurrency.Token -> getIconStateForToken(currency, value.value.isError)
}
}
- private fun getIconStateForCoin(
- coin: CryptoCurrency.Coin,
- isUnreachable: Boolean,
- ): TokenItemState.IconState.CoinIcon {
- return TokenItemState.IconState.CoinIcon(
+ private fun getIconStateForCoin(coin: CryptoCurrency.Coin, isUnreachable: Boolean): TokenIconState.CoinIcon {
+ return TokenIconState.CoinIcon(
url = coin.iconUrl,
fallbackResId = coin.networkIconResId,
isGrayscale = coin.network.isTestnet || isUnreachable,
@@ -29,20 +29,20 @@ internal class CryptoCurrencyToIconStateConverter : Converter
+
+
+
+
+
\ No newline at end of file
diff --git a/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt b/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt
index 692027bfe8..5a44a6614d 100644
--- a/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt
+++ b/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt
@@ -8,5 +8,6 @@ interface SendRouter {
companion object {
const val CRYPTO_CURRENCY_KEY = "send_crypto_currency"
+ const val USER_WALLET_ID_KEY = "send_user_wallet_id"
}
}
\ No newline at end of file
diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts
index 3c6f6bd7f7..9c3a10e456 100644
--- a/features/send/impl/build.gradle.kts
+++ b/features/send/impl/build.gradle.kts
@@ -14,7 +14,12 @@ 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)
+ implementation(deps.tangem.card.core)
/** Compose */
implementation(deps.compose.accompanist.systemUiController)
@@ -23,13 +28,24 @@ 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.models)
+ 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)
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt
index 334bdc5aeb..d33b82b860 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt
@@ -4,10 +4,14 @@ import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.hilt.navigation.compose.hiltViewModel
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.screen.ComposeBottomSheetFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
+import com.tangem.features.send.impl.presentation.send.state.SendUiState
import com.tangem.features.send.impl.presentation.send.ui.SendScreen
+import com.tangem.features.send.impl.presentation.send.viewmodel.SendViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -24,13 +28,16 @@ internal class SendFragment : ComposeBottomSheetFragment() {
@Composable
override fun ScreenContent(modifier: Modifier) {
- SystemBarsEffect {
- setSystemBarsColor(color = Color.Transparent)
+ val viewModel = hiltViewModel()
+ LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
+
+ SystemBarsEffect { setSystemBarsColor(color = Color.Transparent) }
+ BackHandler { dismiss() }
+
+ when (val state = viewModel.uiState) {
+ is SendUiState.Content -> SendScreen(state)
+ SendUiState.Dismiss -> dismiss()
}
- BackHandler {
- dismiss()
- }
- SendScreen()
}
companion object {
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountSegmentedButtonsConfig.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountSegmentedButtonsConfig.kt
new file mode 100644
index 0000000000..ea3220b6e6
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountSegmentedButtonsConfig.kt
@@ -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,
+)
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountStateConverter.kt
new file mode 100644
index 0000000000..c1a17d6011
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountStateConverter.kt
@@ -0,0 +1,63 @@
+package com.tangem.features.send.impl.presentation.send.state
+
+import arrow.core.Either
+import com.tangem.common.Provider
+import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
+import com.tangem.core.ui.extensions.stringReference
+import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount
+import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount
+import com.tangem.domain.appcurrency.model.AppCurrency
+import com.tangem.domain.tokens.error.CurrencyStatusError
+import com.tangem.domain.tokens.model.CryptoCurrencyStatus
+import com.tangem.domain.wallets.models.UserWallet
+import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldConverter
+import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents
+import com.tangem.utils.converter.Converter
+import kotlinx.collections.immutable.persistentListOf
+
+internal class SendAmountStateConverter(
+ private val currentStateProvider: Provider,
+ private val appCurrencyProvider: Provider,
+ private val userWalletProvider: Provider,
+ private val clickIntents: SendClickIntents,
+ private val iconStateConverter: CryptoCurrencyToIconStateConverter,
+ private val sendAmountFieldConverter: SendAmountFieldConverter,
+) : Converter, SendUiState> {
+
+ override fun convert(value: Either): SendUiState {
+ val userWallet = userWalletProvider() ?: return currentStateProvider()
+ val appCurrency = appCurrencyProvider()
+ return value.fold(
+ ifLeft = {
+ // TODO add error handling
+ currentStateProvider()
+ },
+ ifRight = {
+ val fiat = formatFiatAmount(it.value.fiatAmount, appCurrency.code, appCurrency.symbol)
+ val crypto = formatCryptoAmount(it.value.amount, it.currency.symbol, it.currency.decimals)
+ SendUiState.Content.AmountState(
+ cryptoCurrencyStatus = it,
+ walletName = userWallet.name,
+ walletBalance = "$crypto ($fiat)",
+ tokenIconState = iconStateConverter.convert(it),
+ appCurrency = appCurrency,
+ amountTextField = sendAmountFieldConverter.convert(Unit),
+ isFiatValue = false,
+ clickIntents = clickIntents,
+ segmentedButtonConfig = persistentListOf(
+ SendAmountSegmentedButtonsConfig(
+ title = stringReference(it.currency.symbol),
+ iconState = iconStateConverter.convert(it),
+ isFiat = false,
+ ),
+ SendAmountSegmentedButtonsConfig(
+ title = stringReference(appCurrency.code),
+ iconState = iconStateConverter.convert(it),
+ isFiat = true,
+ ),
+ ),
+ )
+ },
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendStateFactory.kt
new file mode 100644
index 0000000000..bfca56ad1d
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendStateFactory.kt
@@ -0,0 +1,58 @@
+package com.tangem.features.send.impl.presentation.send.state
+
+import arrow.core.Either
+import com.tangem.common.Provider
+import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
+import com.tangem.domain.appcurrency.model.AppCurrency
+import com.tangem.domain.tokens.error.CurrencyStatusError
+import com.tangem.domain.tokens.model.CryptoCurrencyStatus
+import com.tangem.domain.wallets.models.UserWallet
+import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldChangeConverter
+import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldConverter
+import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents
+
+internal class SendStateFactory(
+ private val clickIntents: SendClickIntents,
+ private val currentStateProvider: Provider,
+ private val appCurrencyProvider: Provider,
+ private val userWalletProvider: Provider,
+) {
+
+ private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
+
+ private val amountFieldConverter by lazy { SendAmountFieldConverter(clickIntents) }
+
+ private val amountFieldChangeConverter by lazy { SendAmountFieldChangeConverter(currentStateProvider) }
+
+ private val amountStateConverter by lazy {
+ SendAmountStateConverter(
+ currentStateProvider = currentStateProvider,
+ appCurrencyProvider = appCurrencyProvider,
+ clickIntents = clickIntents,
+ iconStateConverter = iconStateConverter,
+ userWalletProvider = userWalletProvider,
+ sendAmountFieldConverter = amountFieldConverter,
+ )
+ }
+
+ fun getInitialState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents)
+
+ fun getAmountState(cryptoCurrencyStatus: Either): SendUiState {
+ return amountStateConverter.convert(cryptoCurrencyStatus)
+ }
+
+ fun getOnReceiveState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents)
+
+ fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value)
+
+ fun getOnCurrencyChangedState(isFiat: Boolean): SendUiState {
+ val state = currentStateProvider()
+ val amountState = state as? SendUiState.Content.AmountState ?: return state
+
+ return if (amountState.isFiatValue == isFiat) {
+ state
+ } else {
+ return state.copy(isFiatValue = isFiat)
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendUiState.kt
new file mode 100644
index 0000000000..0c48ca14b0
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendUiState.kt
@@ -0,0 +1,70 @@
+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 com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents
+import kotlinx.collections.immutable.PersistentList
+
+/**
+ * Ui states of the send screen
+ */
+@Immutable
+internal sealed class SendUiState {
+
+ /** States with content */
+ sealed class Content : SendUiState() {
+
+ /** Is primary button enabled */
+ abstract val isPrimaryButtonEnabled: Boolean
+
+ /** Click intents */
+ abstract val clickIntents: SendClickIntents
+
+ /** Initial state */
+ data class Initial(
+ override val isPrimaryButtonEnabled: Boolean = false,
+ override val clickIntents: SendClickIntents,
+ ) : Content()
+
+ /** Amount state */
+ data class AmountState(
+ override val isPrimaryButtonEnabled: Boolean = false,
+ override val clickIntents: SendClickIntents,
+ val walletName: String,
+ val walletBalance: String,
+ val tokenIconState: TokenIconState,
+ val cryptoCurrencyStatus: CryptoCurrencyStatus,
+ val appCurrency: AppCurrency,
+ val isFiatValue: Boolean,
+ val segmentedButtonConfig: PersistentList,
+ val amountTextField: SendTextField.Amount,
+ ) : Content()
+
+ // todo [REDACTED_JIRA]
+ /** Recipient state */
+ data class RecipientState(
+ override val isPrimaryButtonEnabled: Boolean = false,
+ override val clickIntents: SendClickIntents,
+ ) : Content()
+
+ // todo [REDACTED_JIRA]
+ /** Fee and speed state */
+ data class FeeState(
+ override val isPrimaryButtonEnabled: Boolean = false,
+ override val clickIntents: SendClickIntents,
+ ) : Content()
+
+ // todo [REDACTED_JIRA]
+ /** Send state */
+ data class SendState(
+ override val isPrimaryButtonEnabled: Boolean = true,
+ override val clickIntents: SendClickIntents,
+ ) : Content()
+ }
+
+ /** Dismiss screen */
+ object Dismiss : SendUiState()
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldChangeConverter.kt
new file mode 100644
index 0000000000..0775989341
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldChangeConverter.kt
@@ -0,0 +1,97 @@
+package com.tangem.features.send.impl.presentation.send.state.fields
+
+import com.tangem.common.Provider
+import com.tangem.features.send.impl.presentation.send.state.SendUiState
+import com.tangem.utils.converter.Converter
+import java.text.DecimalFormatSymbols
+import java.text.NumberFormat
+
+internal class SendAmountFieldChangeConverter(
+ private val currentStateProvider: Provider,
+) : Converter {
+ override fun convert(value: String): SendUiState {
+ val state = currentStateProvider()
+
+ if (
+ state !is SendUiState.Content.AmountState ||
+ value.checkDecimalSeparatorDuplicate()
+ ) {
+ return state
+ }
+
+ if (value.isEmpty()) return state.emptyState()
+
+ val fiatRate = state.cryptoCurrencyStatus.value.fiatRate
+
+ val trimmedValue = value.trim()
+
+ val cryptoValue = if (state.isFiatValue) {
+ if (value.isNotBlank()) {
+ trimmedValue.toBigDecimal().divide(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty()
+ } else {
+ DEFAULT_VALUE
+ }
+ } else {
+ trimmedValue
+ }
+
+ val fiatValue = if (!state.isFiatValue) {
+ if (value.isNotBlank()) {
+ trimmedValue.toBigDecimal().multiply(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty()
+ } else {
+ DEFAULT_VALUE
+ }
+ } else {
+ trimmedValue
+ }
+
+ val isExceedBalance = value.checkExceedBalance(state)
+ return state.copy(
+ amountTextField = state.amountTextField.copy(
+ value = cryptoValue,
+ fiatValue = fiatValue,
+ isError = isExceedBalance,
+ ),
+ isPrimaryButtonEnabled = !isExceedBalance,
+ )
+ }
+
+ private fun SendUiState.Content.AmountState.emptyState(): SendUiState {
+ return copy(
+ amountTextField = amountTextField.copy(
+ value = if (!isFiatValue) "" else DEFAULT_VALUE,
+ fiatValue = if (isFiatValue) "" else DEFAULT_VALUE,
+ isError = false,
+ ),
+ isPrimaryButtonEnabled = false,
+ )
+ }
+
+ private fun String.checkDecimalSeparatorDuplicate(): Boolean {
+ val regex = "[\\.\\,]".toRegex()
+ val decimalSeparatorCount = regex.findAll(this).count()
+
+ return decimalSeparatorCount > 1
+ }
+
+ private fun String.checkExceedBalance(state: SendUiState.Content.AmountState): Boolean {
+ val currencyStatus = state.cryptoCurrencyStatus.value
+ return if (state.isFiatValue) {
+ toBigDecimal() > currencyStatus.fiatAmount
+ } else {
+ toBigDecimal() > currencyStatus.amount
+ }
+ }
+
+ private fun String.trim(): String {
+ var trimmedValue = this
+ if (length > 1 && firstOrNull() == '0' && get(1).isDigit()) trimmedValue = drop(1)
+
+ val separatorChar = DecimalFormatSymbols.getInstance().decimalSeparator.toString()
+ return trimmedValue.replace("[\\.\\,]".toRegex(), separatorChar)
+ }
+
+ companion object {
+ private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00)
+ }
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldConverter.kt
new file mode 100644
index 0000000000..8f09a10a04
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldConverter.kt
@@ -0,0 +1,35 @@
+package com.tangem.features.send.impl.presentation.send.state.fields
+
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import com.tangem.core.ui.extensions.TextReference
+import com.tangem.features.send.impl.R
+import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents
+import com.tangem.utils.converter.Converter
+import java.text.NumberFormat
+
+internal class SendAmountFieldConverter(
+ private val clickIntents: SendClickIntents,
+) : Converter {
+
+ override fun convert(value: Unit): SendTextField.Amount {
+ return SendTextField.Amount(
+ value = "",
+ fiatValue = DEFAULT_VALUE,
+ onValueChange = clickIntents::onAmountValueChange,
+ keyboardOptions = KeyboardOptions(
+ imeAction = ImeAction.Next,
+ keyboardType = KeyboardType.Number,
+ ),
+ label = TextReference.Str(""),
+ placeholder = TextReference.Str(DEFAULT_VALUE),
+ isError = false,
+ error = TextReference.Res(R.string.send_insufficient_funds),
+ )
+ }
+
+ companion object {
+ private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00)
+ }
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendTextField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendTextField.kt
new file mode 100644
index 0000000000..e7677e747d
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendTextField.kt
@@ -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()
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendAmountContent.kt
new file mode 100644
index 0000000000..8f6dbcabc9
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendAmountContent.kt
@@ -0,0 +1,109 @@
+package com.tangem.features.send.impl.presentation.send.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+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.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import com.tangem.core.ui.components.SecondaryButton
+import com.tangem.core.ui.components.buttons.common.TangemButtonSize
+import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
+import com.tangem.core.ui.components.currency.fiaticon.FiatIcon
+import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
+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.send.state.SendAmountSegmentedButtonsConfig
+import com.tangem.features.send.impl.presentation.send.state.SendUiState
+import com.tangem.features.send.impl.presentation.send.ui.amount.AmountFieldContainer
+
+@Composable
+internal fun SendAmountContent(amountState: SendUiState.Content.AmountState) {
+ Column(
+ modifier = Modifier
+ .background(TangemTheme.colors.background.tertiary),
+ ) {
+ Text(
+ text = stringResource(R.string.common_send),
+ style = TangemTheme.typography.subtitle1,
+ color = TangemTheme.colors.text.primary1,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .padding(vertical = TangemTheme.dimens.spacing16)
+ .align(CenterHorizontally),
+ )
+ AmountFieldContainer(amountState = amountState)
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ top = TangemTheme.dimens.spacing12,
+ start = TangemTheme.dimens.spacing16,
+ end = TangemTheme.dimens.spacing16,
+ ),
+ ) {
+ SegmentedButtons(
+ modifier = Modifier
+ .height(TangemTheme.dimens.size40)
+ .weight(1f),
+ config = amountState.segmentedButtonConfig,
+ onClick = { amountState.clickIntents.onCurrencyChangeClick(it.isFiat) },
+ ) {
+ SendAmountCurrencyButton(it)
+ }
+ SecondaryButton(
+ text = stringResource(R.string.send_max_amount),
+ onClick = amountState.clickIntents::onMaxValueClick,
+ size = TangemButtonSize.Text,
+ shape = RoundedCornerShape(TangemTheme.dimens.radius26),
+ modifier = Modifier
+ .padding(start = TangemTheme.dimens.spacing8)
+ .height(TangemTheme.dimens.size40),
+ )
+ }
+ }
+}
+
+@Composable
+private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) {
+ Row(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(
+ horizontal = TangemTheme.dimens.spacing10,
+ ),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ if (button.isFiat) {
+ FiatIcon(
+ url = button.iconUrl,
+ modifier = Modifier
+ .size(TangemTheme.dimens.size18),
+ )
+ } else {
+ button.iconState?.let {
+ TokenIcon(
+ state = it,
+ shouldDisplayNetwork = false,
+ modifier = Modifier
+ .size(TangemTheme.dimens.size18),
+ )
+ }
+ }
+ Text(
+ text = button.title.resolveReference(),
+ color = TangemTheme.colors.text.primary1,
+ style = TangemTheme.typography.button,
+ modifier = Modifier
+ .padding(
+ start = TangemTheme.dimens.spacing8,
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendBottomSheetConfig.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendBottomSheetConfig.kt
deleted file mode 100644
index 73f5797f7b..0000000000
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendBottomSheetConfig.kt
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt
index 7630c89501..cf453d487a 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt
@@ -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.isPrimaryButtonEnabled,
+ onClick = {
+ // todo add next click
+ },
+ )
+ } else {
+ PrimaryButton(
+ text = stringResource(textId),
+ enabled = uiState.isPrimaryButtonEnabled,
onClick = {
// todo add next click
},
)
- SendStates.Filled -> {
- PrimaryButtonIconEnd(
- text = stringResource(textId),
- iconResId = R.drawable.ic_tangem_24,
- onClick = {
- // todo add next click
- },
- )
- }
}
}
}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt
index 3ec40b3169..1e52cf291a 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt
@@ -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) }
-
+internal fun SendScreen(uiState: SendUiState.Content) {
Column(
modifier = Modifier
.imePadding()
@@ -40,13 +37,17 @@ fun SendScreen() {
.weight(1f)
.scrollable(state = rememberScrollState(), orientation = Orientation.Vertical),
) {
- SendScreenContent()
+ SendScreenContent(uiState)
}
- SendNavigationButtons(config.currentState)
+ SendNavigationButtons(uiState)
}
}
@Composable
-fun SendScreenContent() {
- // todo work in progress
+private fun SendScreenContent(uiState: SendUiState.Content) {
+ when (uiState) {
+ is SendUiState.Content.AmountState -> SendAmountContent(uiState)
+ else -> { /* [REDACTED_TODO_COMMENT]*/
+ }
+ }
}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendStates.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendStates.kt
deleted file mode 100644
index f0a3e9840b..0000000000
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendStates.kt
+++ /dev/null
@@ -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
- }
- }
- }
-}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountField.kt
new file mode 100644
index 0000000000..4eda379a95
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountField.kt
@@ -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
+ }
+ },
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountFieldContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountFieldContainer.kt
new file mode 100644
index 0000000000..b7810d5acb
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountFieldContainer.kt
@@ -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,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendClickIntents.kt
new file mode 100644
index 0000000000..3e6885543a
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendClickIntents.kt
@@ -0,0 +1,14 @@
+package com.tangem.features.send.impl.presentation.send.viewmodel
+
+interface SendClickIntents {
+
+ fun onNextClick()
+
+ fun onPrevClick()
+
+ fun onAmountValueChange(value: String)
+
+ fun onCurrencyChangeClick(isFiat: Boolean)
+
+ fun onMaxValueClick()
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendViewModel.kt
new file mode 100644
index 0000000000..5bc0189731
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendViewModel.kt
@@ -0,0 +1,155 @@
+package com.tangem.features.send.impl.presentation.send.viewmodel
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.*
+import arrow.core.getOrElse
+import com.tangem.common.Provider
+import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
+import com.tangem.domain.appcurrency.model.AppCurrency
+import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
+import com.tangem.domain.tokens.model.CryptoCurrency
+import com.tangem.domain.wallets.models.UserWallet
+import com.tangem.domain.wallets.models.UserWalletId
+import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
+import com.tangem.features.send.api.navigation.SendRouter
+import com.tangem.features.send.impl.presentation.send.state.SendStateFactory
+import com.tangem.features.send.impl.presentation.send.state.SendUiState
+import com.tangem.utils.coroutines.CoroutineDispatcherProvider
+import com.tangem.utils.coroutines.JobHolder
+import com.tangem.utils.coroutines.saveIn
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+internal class SendViewModel @Inject constructor(
+ private val dispatchers: CoroutineDispatcherProvider,
+ private val getUserWalletUseCase: GetUserWalletUseCase,
+ private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
+ private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
+ savedStateHandle: SavedStateHandle,
+) : ViewModel(), DefaultLifecycleObserver, SendClickIntents {
+
+ private val userWalletId: UserWalletId = savedStateHandle.get(SendRouter.USER_WALLET_ID_KEY)
+ ?.let { stringValue -> UserWalletId(stringValue) }
+ ?: error("This screen can't open without `UserWalletId`")
+
+ private val cryptoCurrency: CryptoCurrency = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY]
+ ?: error("This screen can't open without `CryptoCurrency`")
+
+ private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow()
+
+ private val stateFactory = SendStateFactory(
+ clickIntents = this,
+ currentStateProvider = Provider { uiState },
+ userWalletProvider = Provider { userWallet },
+ appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
+ )
+
+ var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState())
+ private set
+
+ private var userWallet: UserWallet? = null
+
+ private var balanceJobHolder = JobHolder()
+
+ override fun onCreate(owner: LifecycleOwner) {
+ subscribeOnCurrencyStatusUpdates(owner)
+ }
+
+ private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) {
+ viewModelScope.launch(dispatchers.io) {
+ getUserWalletUseCase(userWalletId).fold(
+ ifRight = { wallet ->
+ userWallet = wallet
+ getCurrencyStatusUpdates(owner, wallet)
+ },
+ ifLeft = {
+ // TODO add error handling
+ return@launch
+ },
+ )
+ }
+ }
+
+ private fun getCurrencyStatusUpdates(owner: LifecycleOwner, wallet: UserWallet) {
+ val isSingleWallet = wallet.scanResponse.walletData?.token != null && !wallet.isMultiCurrency
+ getCurrencyStatusUpdatesUseCase(
+ userWalletId = userWalletId,
+ currencyId = cryptoCurrency.id,
+ derivationPath = cryptoCurrency.network.derivationPath,
+ isSingleWalletWithTokens = isSingleWallet,
+ )
+ .flowWithLifecycle(owner.lifecycle)
+ .conflate()
+ .distinctUntilChanged()
+ .onEach { either ->
+ uiState = stateFactory.getAmountState(
+ cryptoCurrencyStatus = either,
+ )
+ }
+ .flowOn(dispatchers.io)
+ .launchIn(viewModelScope)
+ .saveIn(balanceJobHolder)
+ }
+
+ private fun createSelectedAppCurrencyFlow(): StateFlow {
+ return getSelectedAppCurrencyUseCase()
+ .map { maybeAppCurrency ->
+ maybeAppCurrency.getOrElse { AppCurrency.Default }
+ }
+ .stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.Eagerly,
+ initialValue = AppCurrency.Default,
+ )
+ }
+
+ // region screen state navigation
+ override fun onNextClick() {
+ when (uiState) {
+ is SendUiState.Content.AmountState -> onRecipientStateClick()
+ is SendUiState.Content.RecipientState -> onFeeStateClick()
+ else -> {
+ // todo implement
+ }
+ }
+ }
+
+ override fun onPrevClick() {
+ // todo implement
+ }
+
+ private fun onRecipientStateClick() {
+ stateFactory.getOnReceiveState()
+ }
+
+ private fun onFeeStateClick() {
+ // todo implement
+ }
+ // endregion
+
+ // region amount state clicks
+ override fun onCurrencyChangeClick(isFiat: Boolean) {
+ uiState = stateFactory.getOnCurrencyChangedState(isFiat)
+ }
+
+ override fun onAmountValueChange(value: String) {
+ uiState = stateFactory.getOnAmountValueChange(value)
+ }
+
+ override fun onMaxValueClick() {
+ val amountState = uiState as? SendUiState.Content.AmountState ?: return
+
+ val amount = if (amountState.isFiatValue) {
+ amountState.cryptoCurrencyStatus.value.fiatAmount
+ } else {
+ amountState.cryptoCurrencyStatus.value.amount
+ }
+ onAmountValueChange(amount?.toPlainString() ?: "0.00")
+ }
+ // endregion
+}
\ No newline at end of file
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt
index 4506069426..15c78e5b89 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt
@@ -5,6 +5,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
+import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.consumedEvent
@@ -87,7 +88,7 @@ internal object WalletPreviewData {
}
val coinIconState
- get() = TokenItemState.IconState.CoinIcon(
+ get() = TokenIconState.CoinIcon(
url = null,
fallbackResId = R.drawable.img_polygon_22,
isGrayscale = false,
@@ -95,7 +96,7 @@ internal object WalletPreviewData {
)
private val tokenIconState
- get() = TokenItemState.IconState.TokenIcon(
+ get() = TokenIconState.TokenIcon(
url = null,
networkBadgeIconResId = R.drawable.img_polygon_22,
fallbackTint = TangemColorPalette.Black,
@@ -105,7 +106,7 @@ internal object WalletPreviewData {
)
private val customTokenIconState
- get() = TokenItemState.IconState.CustomTokenIcon(
+ get() = TokenIconState.CustomTokenIcon(
tint = TangemColorPalette.Black,
background = TangemColorPalette.Meadow,
networkBadgeIconResId = R.drawable.img_polygon_22,
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt
index 9e0ea7e0cd..fbc0f7af7b 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt
@@ -14,11 +14,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Constraints
import com.tangem.core.ui.components.marketprice.PriceChangeType
+import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.common.component.token.*
-import com.tangem.feature.wallet.presentation.common.component.token.icon.TokenIcon
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import org.burnoutcrew.reorderable.ReorderableLazyListState
import kotlin.math.max
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt
index b9f7b9d127..52bdd7abed 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt
@@ -1,9 +1,8 @@
package com.tangem.feature.wallet.presentation.common.state
-import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
-import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.components.marketprice.PriceChangeType
+import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
/** Token item state */
@Immutable
@@ -11,7 +10,7 @@ internal sealed class TokenItemState {
abstract val id: String
- abstract val iconState: IconState
+ abstract val iconState: TokenIconState
abstract val titleState: TitleState
@@ -24,7 +23,7 @@ internal sealed class TokenItemState {
/** Loading token state */
data class Loading(
override val id: String,
- override val iconState: IconState,
+ override val iconState: TokenIconState,
override val titleState: TitleState.Content,
) : TokenItemState() {
override val fiatAmountState: FiatAmountState = FiatAmountState.Loading
@@ -34,7 +33,7 @@ internal sealed class TokenItemState {
/** Locked token state */
data class Locked(override val id: String) : TokenItemState() {
- override val iconState: IconState = IconState.Locked
+ override val iconState: TokenIconState = TokenIconState.Locked
override val titleState: TitleState = TitleState.Locked
override val fiatAmountState: FiatAmountState = FiatAmountState.Locked
override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Locked
@@ -52,7 +51,7 @@ internal sealed class TokenItemState {
*/
data class Content(
override val id: String,
- override val iconState: IconState,
+ override val iconState: TokenIconState,
override val titleState: TitleState,
override val fiatAmountState: FiatAmountState,
override val cryptoAmountState: CryptoAmountState.Content,
@@ -70,7 +69,7 @@ internal sealed class TokenItemState {
*/
data class Draggable(
override val id: String,
- override val iconState: IconState,
+ override val iconState: TokenIconState,
override val titleState: TitleState,
override val cryptoAmountState: CryptoAmountState,
) : TokenItemState() {
@@ -89,7 +88,7 @@ internal sealed class TokenItemState {
*/
data class Unreachable(
override val id: String,
- override val iconState: IconState,
+ override val iconState: TokenIconState,
override val titleState: TitleState,
val onItemClick: () -> Unit,
val onItemLongClick: () -> Unit,
@@ -109,7 +108,7 @@ internal sealed class TokenItemState {
*/
data class NoAddress(
override val id: String,
- override val iconState: IconState,
+ override val iconState: TokenIconState,
override val titleState: TitleState,
val onItemLongClick: () -> Unit,
) : TokenItemState() {
@@ -118,84 +117,6 @@ internal sealed class TokenItemState {
override val priceChangeState: PriceChangeState? = null
}
- /**
- * Represents the various states an icon can be in.
- */
- @Immutable
- sealed class IconState {
-
- abstract val isGrayscale: Boolean
- abstract val showCustomBadge: Boolean
- abstract val networkBadgeIconResId: Int?
-
- /**
- * Represents a coin icon.
- *
- * @property url The URL where the coin icon can be fetched from. May be `null` if not found.
- * @property fallbackResId The drawable resource ID to be used as a fallback if the URL is not available.
- * @property isGrayscale Specifies whether to show the icon in grayscale.
- * @property showCustomBadge Specifies whether to show the custom token badge.
- */
- data class CoinIcon(
- val url: String?,
- @DrawableRes val fallbackResId: Int,
- override val isGrayscale: Boolean,
- override val showCustomBadge: Boolean,
- ) : IconState() {
-
- override val networkBadgeIconResId: Int? = null
- }
-
- /**
- * Represents a token icon.
- *
- * @property url The URL where the token icon can be fetched from. May be `null` if not found.
- * @property networkBadgeIconResId The drawable resource ID for the network badge.
- * @property isGrayscale Specifies whether to show the icon in grayscale.
- * @property showCustomBadge Specifies whether to show the custom token badge.
- * @property fallbackTint The color to be used for tinting the fallback icon.
- * @property fallbackBackground The background color to be used for the fallback icon.
- */
- data class TokenIcon(
- val url: String?,
- @DrawableRes override val networkBadgeIconResId: Int,
- override val isGrayscale: Boolean,
- override val showCustomBadge: Boolean,
- val fallbackTint: Color,
- val fallbackBackground: Color,
- ) : IconState()
-
- /**
- * Represents a custom token icon.
- *
- * @property tint The color to be used for tinting the icon.
- * @property background The background color to be used for the icon.
- * @property networkBadgeIconResId The drawable resource ID for the network badge.
- * @property isGrayscale Specifies whether to show the icon in grayscale.
- */
- data class CustomTokenIcon(
- val tint: Color,
- val background: Color,
- @DrawableRes override val networkBadgeIconResId: Int,
- override val isGrayscale: Boolean,
- ) : IconState() {
-
- override val showCustomBadge: Boolean = true
- }
-
- object Loading : IconState() {
- override val isGrayscale: Boolean = false
- override val showCustomBadge: Boolean = false
- override val networkBadgeIconResId: Int? = null
- }
-
- object Locked : IconState() {
- override val isGrayscale: Boolean = false
- override val showCustomBadge: Boolean = false
- override val networkBadgeIconResId: Int? = null
- }
- }
-
@Immutable
sealed class TitleState {
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt
index aa518deba2..60445f1928 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt
@@ -5,7 +5,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
-import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter
+import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId
import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt
index 59b3b14b1e..df51275678 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt
@@ -6,7 +6,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
-import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter
+import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
import com.tangem.utils.converter.Converter
import java.math.BigDecimal