diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml
index 2af2ae2c68..52aa5dfca4 100644
--- a/core/res/src/main/res/values-ru/strings.xml
+++ b/core/res/src/main/res/values-ru/strings.xml
@@ -395,7 +395,7 @@
Сумма к получению %1$s
Адрес
Код назначения
- %1$s в %2$s
+ %1$s в %2$s
Введите адрес
Адрес совпадает с адресом кошелька
Недопустимый Tag. Он не будет добавлен в транзакцию.
@@ -427,7 +427,7 @@
Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению.
Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s.
Сумма резерва не может быть менее %1$s.
- Пожалуйста, пополните свой баланс, чтобы продолжить.
+ Пожалуйста, пополните свой баланс, чтобы продолжить.
Увеличение комиссии
Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01.
Комиссия превышает баланс
diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index 020371fd62..09f3ec83d1 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -395,7 +395,7 @@
The recipient will receive %1$s
Address
Destination Tag
- %1$s at %2$s
+ %1$s at %2$s
Enter address
Address is the same as wallet address
Invalid Tag. It won\'t be added to the transaction.
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
index 5868a39017..ab9b42cd1f 100644
--- a/core/ui/build.gradle.kts
+++ b/core/ui/build.gradle.kts
@@ -14,6 +14,7 @@ dependencies {
/** Project - Core */
implementation(projects.core.res)
+ implementation(projects.core.utils)
/** AndroidX libraries */
implementation(deps.androidx.fragment.ktx)
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt
new file mode 100644
index 0000000000..73509da21f
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt
@@ -0,0 +1,103 @@
+package com.tangem.core.ui.components.appbar
+
+import androidx.annotation.DrawableRes
+import androidx.compose.animation.*
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.Icon
+import androidx.compose.material.Text
+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.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.tangem.core.ui.R
+import com.tangem.core.ui.res.TangemTheme
+
+@Composable
+fun AppBarWithBackButtonAndIcon(
+ onBackClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ text: String? = null,
+ @DrawableRes backIconRes: Int? = null,
+ @DrawableRes iconRes: Int? = null,
+ onIconClick: (() -> Unit)? = null,
+ backgroundColor: Color = TangemTheme.colors.background.secondary,
+) {
+ Row(
+ modifier = modifier
+ .background(color = backgroundColor)
+ .fillMaxWidth()
+ .padding(all = TangemTheme.dimens.spacing16),
+ horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ painter = painterResource(backIconRes ?: R.drawable.ic_back_24),
+ contentDescription = null,
+ modifier = Modifier
+ .size(size = TangemTheme.dimens.size24)
+ .clickable { onBackClick() },
+ tint = TangemTheme.colors.icon.primary1,
+ )
+ AnimatedContent(
+ targetState = text,
+ modifier = Modifier.weight(1f),
+ transitionSpec = { fadeIn().togetherWith(fadeOut()) },
+ label = "Toolbar title change",
+ ) {
+ if (!it.isNullOrBlank()) {
+ Text(
+ text = it,
+ color = TangemTheme.colors.text.primary1,
+ maxLines = 1,
+ style = TangemTheme.typography.subtitle1,
+ )
+ }
+ }
+ AnimatedContent(
+ targetState = iconRes,
+ transitionSpec = { (fadeIn() + scaleIn()).togetherWith(fadeOut() + scaleOut()) },
+ label = "Toolbar icon change",
+ ) {
+ if (onIconClick != null && it != null) {
+ Icon(
+ painter = painterResource(it),
+ contentDescription = null,
+ modifier = Modifier
+ .size(size = TangemTheme.dimens.size24)
+ .clickable { onIconClick() },
+ tint = TangemTheme.colors.icon.primary1,
+ )
+ }
+ }
+ }
+}
+
+@Preview(widthDp = 360, heightDp = 56, showBackground = true)
+@Composable
+private fun PreviewAppBarWithBackButtonAndIconInLightTheme() {
+ TangemTheme(isDark = false) {
+ AppBarWithBackButtonAndIcon(
+ text = "Title",
+ iconRes = R.drawable.ic_qrcode_scan_24,
+ onBackClick = {},
+ onIconClick = {},
+ )
+ }
+}
+
+@Preview(widthDp = 360, heightDp = 56, showBackground = true)
+@Composable
+private fun PreviewAppBarWithBackButtonAndIconInDarkTheme() {
+ TangemTheme(isDark = true) {
+ AppBarWithBackButtonAndIcon(
+ text = "Title",
+ iconRes = R.drawable.ic_qrcode_scan_24,
+ onBackClick = {},
+ onIconClick = {},
+ )
+ }
+}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountVisualTransformation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountVisualTransformation.kt
new file mode 100644
index 0000000000..89e4fa09ce
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountVisualTransformation.kt
@@ -0,0 +1,32 @@
+package com.tangem.core.ui.components.fields
+
+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
+
+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/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt
index 80e9488f0a..dc27c1641a 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt
@@ -9,12 +9,14 @@ import java.util.Locale
@Suppress("MagicNumber")
object DateTimeFormatters {
+ private const val DDMMYYYY = "dd.MM.yyyy"
+
/**
* Two SS means, SHORT style for date and time.
* If pattern contains "a", it means time is in 12 hour format.
* [Documentation](https://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html)
*/
- val timeFormatter by lazy {
+ val timeFormatter: DateTimeFormatter by lazy {
val is12HourFormat = DateTimeFormat.patternForStyle("SS", Locale.getDefault()).contains("a")
if (is12HourFormat) {
DateTimeFormatterBuilder()
@@ -35,7 +37,7 @@ object DateTimeFormatters {
}
}
- val dateFormatter by lazy {
+ val dateFormatter: DateTimeFormatter by lazy {
DateTimeFormatterBuilder()
.appendDayOfMonth(1)
.appendLiteral(' ')
@@ -46,6 +48,13 @@ object DateTimeFormatters {
.withLocale(Locale.getDefault())
}
+ val dateDDMMYYYY: DateTimeFormatter by lazy {
+ DateTimeFormatterBuilder()
+ .appendPattern(DDMMYYYY)
+ .toFormatter()
+ .withLocale(Locale.getDefault())
+ }
+
fun formatTime(formatter: DateTimeFormatter = timeFormatter, time: DateTime): String {
return formatter.print(time)
}
diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt
new file mode 100644
index 0000000000..b0237e897a
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt
@@ -0,0 +1,33 @@
+package com.tangem.core.ui.utils
+
+import android.text.format.DateUtils
+import com.tangem.utils.extensions.isToday
+import com.tangem.utils.extensions.isYesterday
+import org.joda.time.DateTime
+import org.joda.time.DateTimeZone
+import org.joda.time.format.DateTimeFormatter
+
+/**
+ * If [this] timestamp is today or yesterday, returns relative date,
+ * otherwise returns formatting date.
+ */
+fun Long.toDateFormat(formatter: DateTimeFormatter = DateTimeFormatters.dateFormatter): String {
+ val localDate = DateTime(this, DateTimeZone.getDefault())
+ return if (localDate.isToday() || localDate.isYesterday()) {
+ DateUtils.getRelativeTimeSpanString(
+ this,
+ DateTime.now().millis,
+ DateUtils.DAY_IN_MILLIS,
+ DateUtils.FORMAT_ABBREV_RELATIVE,
+ ).toString()
+ } else {
+ DateTimeFormatters.formatDate(formatter = formatter, date = localDate)
+ }
+}
+
+/**
+ * Returns formatted time according to [formatter].
+ */
+fun Long.toTimeFormat(formatter: DateTimeFormatter = DateTimeFormatters.timeFormatter): String {
+ return DateTimeFormatters.formatTime(formatter = formatter, time = DateTime(this, DateTimeZone.getDefault()))
+}
\ No newline at end of file
diff --git a/core/ui/src/main/res/drawable/ic_qrcode_scan_24.xml b/core/ui/src/main/res/drawable/ic_qrcode_scan_24.xml
new file mode 100644
index 0000000000..7cde4123e0
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_qrcode_scan_24.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt
index f9bc744566..bc1bf00b14 100644
--- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt
@@ -323,6 +323,10 @@ class DefaultWalletManagersFacade(
}
override suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
{
+ return getAddresses(userWalletId, network).sortedBy { it.type }
+ }
+
+ override suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set {
val blockchain = Blockchain.fromId(network.id.value)
return getOrCreateWalletManager(
@@ -332,7 +336,6 @@ class DefaultWalletManagersFacade(
)
?.wallet
?.addresses
- ?.sortedBy { it.type }
.orEmpty()
}
diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt
index e4817280a7..31345d6f99 100644
--- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt
@@ -114,6 +114,13 @@ interface WalletManagersFacade {
*/
suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
+ /** Returns list of all addresses for all currencies in selected wallet
+ *
+ * @param userWalletId selected wallet id
+ * @param network required to create wallet manager
+ */
+ suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set
+
/**
* Returns info about rent if wallet manager implemented [RentProvider], otherwise null
*
diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts
index 9c3a10e456..65f362206d 100644
--- a/features/send/impl/build.gradle.kts
+++ b/features/send/impl/build.gradle.kts
@@ -14,12 +14,14 @@ dependencies {
/** AndroidX */
implementation(deps.androidx.fragment.ktx)
implementation(deps.androidx.appCompat)
+ implementation(deps.androidx.paging.runtime)
/** Other dependencies */
implementation(deps.kotlin.immutable.collections)
implementation(deps.material)
implementation(deps.arrow.core)
- implementation(deps.tangem.card.core)
+ implementation(deps.lifecycle.compose)
+ implementation(deps.jodatime)
/** Compose */
implementation(deps.compose.accompanist.systemUiController)
@@ -29,6 +31,12 @@ dependencies {
implementation(deps.compose.ui.tooling)
implementation(deps.compose.navigation)
implementation(deps.compose.navigation.hilt)
+ implementation(deps.compose.paging)
+ implementation(deps.compose.constraintLayout)
+
+ /** Tangem SDKs */
+ implementation(deps.tangem.card.core)
+ implementation(deps.tangem.blockchain)
/** Common */
implementation(projects.common)
@@ -40,12 +48,15 @@ dependencies {
/** Domain modules */
implementation(projects.domain.models)
+ implementation(projects.domain.legacy)
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)
+ implementation(projects.domain.txhistory)
+ implementation(projects.domain.txhistory.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 61136054f5..2790caab93 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
@@ -1,43 +1,53 @@
package com.tangem.features.send.impl.presentation
-import androidx.activity.compose.BackHandler
+import android.os.Bundle
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 androidx.fragment.app.viewModels
import com.tangem.core.ui.components.SystemBarsEffect
-import com.tangem.core.ui.screen.ComposeBottomSheetFragment
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
-import com.tangem.features.send.impl.presentation.state.SendUiState
+import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.features.send.impl.presentation.ui.SendScreen
import com.tangem.features.send.impl.presentation.viewmodel.SendViewModel
import dagger.hilt.android.AndroidEntryPoint
+import java.lang.ref.WeakReference
import javax.inject.Inject
/**
* Send fragment
*/
@AndroidEntryPoint
-internal class SendFragment : ComposeBottomSheetFragment() {
+internal class SendFragment : ComposeFragment() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
- override val expandedHeightFraction: Float = 1f
+ private val viewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycle.addObserver(viewModel)
+ viewModel.setRouter(
+ StateRouter(
+ fragmentManager = WeakReference(parentFragmentManager),
+ ),
+ )
+ }
@Composable
override fun ScreenContent(modifier: Modifier) {
- 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()
+ val systemBarsColor = TangemTheme.colors.background.tertiary
+ SystemBarsEffect {
+ setSystemBarsColor(systemBarsColor)
}
+ SendScreen(viewModel.uiState)
+ }
+
+ override fun onDestroy() {
+ lifecycle.removeObserver(viewModel)
+ super.onDestroy()
}
companion object {
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt
new file mode 100644
index 0000000000..69e56bfc62
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt
@@ -0,0 +1,15 @@
+package com.tangem.features.send.impl.presentation.domain
+
+import androidx.compose.runtime.Immutable
+
+/**
+ * Available wallet to send
+ *
+ * @property name wallet name
+ * @property address blockchain address
+ */
+@Immutable
+data class AvailableWallet(
+ val name: String,
+ val address: String,
+)
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt
new file mode 100644
index 0000000000..6cb3516497
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt
@@ -0,0 +1,21 @@
+package com.tangem.features.send.impl.presentation.domain
+
+import androidx.annotation.DrawableRes
+import androidx.compose.runtime.Immutable
+import com.tangem.core.ui.extensions.TextReference
+import kotlinx.collections.immutable.PersistentList
+
+@Immutable
+internal sealed class SendRecipientListContent {
+ data class Item(
+ val id: String,
+ val title: TextReference,
+ val subtitle: TextReference,
+ val info: TextReference? = null,
+ @DrawableRes val subtitleIconRes: Int? = null,
+ ) : SendRecipientListContent()
+
+ data class Wallets(
+ val list: PersistentList- ,
+ ) : SendRecipientListContent()
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt
index d60ad8e28b..87ac690720 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt
@@ -1,22 +1,35 @@
package com.tangem.features.send.impl.presentation.state
-import arrow.core.Either
+import androidx.paging.PagingData
+import com.tangem.blockchain.common.address.Address
import com.tangem.common.Provider
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
+import com.tangem.core.ui.extensions.TextReference
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.txhistory.models.TxHistoryItem
import com.tangem.domain.wallets.models.UserWallet
+import com.tangem.features.send.impl.R
+import com.tangem.features.send.impl.presentation.domain.AvailableWallet
import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
+import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientListConverter
+import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
+import com.tangem.features.send.impl.presentation.viewmodel.isNotAddressInWallet
+import com.tangem.features.send.impl.presentation.viewmodel.validateMemo
+import com.tangem.features.send.impl.presentation.viewmodel.verifyAddress
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
internal class SendStateFactory(
private val clickIntents: SendClickIntents,
private val currentStateProvider: Provider,
+ private val userWalletProvider: Provider,
+ private val walletAddressesProvider: Provider>,
private val appCurrencyProvider: Provider,
- private val userWalletProvider: Provider,
+ private val cryptoCurrencyStatusProvider: Provider,
) {
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
@@ -27,33 +40,129 @@ internal class SendStateFactory(
private val amountStateConverter by lazy {
SendAmountStateConverter(
- currentStateProvider = currentStateProvider,
appCurrencyProvider = appCurrencyProvider,
- clickIntents = clickIntents,
iconStateConverter = iconStateConverter,
userWalletProvider = userWalletProvider,
sendAmountFieldConverter = amountFieldConverter,
+ cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
}
- fun getInitialState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents)
-
- fun getAmountState(cryptoCurrencyStatus: Either): SendUiState {
- return amountStateConverter.convert(cryptoCurrencyStatus)
+ private val recipientStateConverter by lazy {
+ SendRecipientStateConverter(
+ clickIntents = clickIntents,
+ cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
+ )
}
- fun getOnReceiveState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents)
+ private val recipientListStateConverter by lazy {
+ SendRecipientListConverter(
+ currentStateProvider = currentStateProvider,
+ cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
+ )
+ }
+ // region UI states
+ fun getInitialState(): SendUiState = SendUiState(
+ clickIntents = clickIntents,
+ currentState = MutableStateFlow(SendUiStateType.Amount),
+ )
+
+ fun getReadyState(): SendUiState = currentStateProvider().copy(
+ amountState = amountStateConverter.convert(Unit),
+ recipientState = recipientStateConverter.convert(Unit),
+ feeState = SendStates.FeeState(),
+ )
+ //endregion
+
+ //region amount state clicks
fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value)
fun getOnCurrencyChangedState(isFiat: Boolean): SendUiState {
val state = currentStateProvider()
- val amountState = state as? SendUiState.Content.AmountState ?: return state
+ val amountState = state.amountState ?: return state
return if (amountState.isFiatValue == isFiat) {
state
} else {
- return state.copy(isFiatValue = isFiat)
+ return state.copy(amountState = amountState.copy(isFiatValue = isFiat))
}
}
+ //endregion
+
+ //region recipient
+ fun onLoadedRecipientList(wallets: List, txHistory: PagingData) {
+ recipientListStateConverter.convert(
+ wallets = wallets,
+ txHistory = txHistory,
+ )
+ }
+
+ fun getOnRecipientAddressValueChangeState(value: String): SendUiState {
+ val state = currentStateProvider()
+ val recipientState = state.recipientState ?: return state
+
+ val isValidMemo = validateMemo(
+ memo = value,
+ cryptoCurrency = cryptoCurrencyStatusProvider().currency,
+ )
+ val isAddressInWallet = isNotAddressInWallet(
+ walletAddresses = walletAddressesProvider(),
+ address = recipientState.addressTextField.value.value,
+ )
+ val isValidAddress = verifyAddress(
+ address = recipientState.addressTextField.value.value,
+ cryptoCurrency = cryptoCurrencyStatusProvider().currency,
+ )
+
+ recipientState.addressTextField.update {
+ it.copy(
+ value = value,
+ error = when {
+ !isValidAddress -> TextReference.Res(R.string.send_recipient_address_error)
+ !isAddressInWallet -> TextReference.Res(R.string.send_recipient_address_error)
+ else -> null
+ },
+ isError = !isValidAddress || !isAddressInWallet,
+ )
+ }
+ return state.copy(
+ recipientState = recipientState.copy(
+ isPrimaryButtonEnabled = isValidMemo && isValidAddress && isAddressInWallet,
+ ),
+ )
+ }
+
+ fun getOnRecipientMemoValueChangeState(value: String): SendUiState {
+ val state = currentStateProvider()
+ val recipientState = state.recipientState ?: return state
+
+ val isValidMemo = validateMemo(
+ memo = value,
+ cryptoCurrency = cryptoCurrencyStatusProvider().currency,
+ )
+ val isAddressInWallet = isNotAddressInWallet(
+ walletAddresses = walletAddressesProvider(),
+ address = recipientState.addressTextField.value.value,
+ )
+ val isValidAddress = verifyAddress(
+ address = recipientState.addressTextField.value.value,
+ cryptoCurrency = cryptoCurrencyStatusProvider().currency,
+ )
+
+ // todo add memo validation error text
+ recipientState.memoTextField?.update {
+ it.copy(
+ value = value,
+ error = TextReference.Res(R.string.send_memo_destination_tag_error),
+ isError = !isValidMemo,
+ )
+ }
+ return state.copy(
+ recipientState = recipientState.copy(
+ isPrimaryButtonEnabled = isValidMemo && isValidAddress && isAddressInWallet,
+ ),
+ )
+ }
+ //endregion
}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt
index f4c9014b85..6baeb27cfb 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt
@@ -1,71 +1,77 @@
package com.tangem.features.send.impl.presentation.state
import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.paging.PagingData
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.domain.SendRecipientListContent
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import kotlinx.collections.immutable.PersistentList
+import kotlinx.coroutines.flow.MutableStateFlow
/**
* Ui states of the send screen
*/
@Immutable
-internal sealed class SendUiState {
+internal data class SendUiState(
+ val clickIntents: SendClickIntents,
+ val amountState: SendStates.AmountState? = null,
+ val recipientState: SendStates.RecipientState? = null,
+ val feeState: SendStates.FeeState? = null,
+ val recipientList: MutableStateFlow> = MutableStateFlow(PagingData.empty()),
+ val currentState: MutableStateFlow,
+)
- /** States with content */
- sealed class Content : SendUiState() {
+@Stable
+internal sealed class SendStates {
- /** Is primary button enabled */
- abstract val isPrimaryButtonEnabled: Boolean
+ abstract val type: SendUiStateType
- /** Click intents */
- abstract val clickIntents: SendClickIntents
+ /** Amount state */
+ data class AmountState(
+ override val type: SendUiStateType = SendUiStateType.Amount,
+ val cryptoCurrencyStatus: CryptoCurrencyStatus,
+ val appCurrency: AppCurrency,
+ val walletName: String,
+ val walletBalance: String,
+ val tokenIconState: TokenIconState,
+ val isFiatValue: Boolean,
+ val segmentedButtonConfig: PersistentList,
+ val amountTextField: MutableStateFlow,
+ val isPrimaryButtonEnabled: Boolean,
+ ) : SendStates()
- /** Initial state */
- data class Initial(
- override val isPrimaryButtonEnabled: Boolean = false,
- override val clickIntents: SendClickIntents,
- ) : Content()
+ /** Recipient state */
+ data class RecipientState(
+ override val type: SendUiStateType = SendUiStateType.Recipient,
+ val addressTextField: MutableStateFlow,
+ val memoTextField: MutableStateFlow?,
+ val recipients: MutableStateFlow> = MutableStateFlow(PagingData.empty()),
+ val network: String,
+ val isPrimaryButtonEnabled: Boolean,
+ ) : SendStates()
- /** 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]
+ /** Fee and speed state */
+ data class FeeState(
+ override val type: SendUiStateType = SendUiStateType.Fee,
+ ) : SendStates()
- // todo [REDACTED_JIRA]
- /** Recipient state */
- data class RecipientState(
- override val isPrimaryButtonEnabled: Boolean = false,
- override val clickIntents: SendClickIntents,
- ) : Content()
+ // todo [REDACTED_JIRA]
+ /** Send state */
+ data class SendState(
+ val isSuccess: Boolean,
+ )
+}
- // 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()
+enum class SendUiStateType {
+ Amount,
+ Recipient,
+ Fee,
+ Send,
+ Done,
}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt
new file mode 100644
index 0000000000..e7526fdf04
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt
@@ -0,0 +1,39 @@
+package com.tangem.features.send.impl.presentation.state
+
+import androidx.fragment.app.FragmentManager
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import java.lang.ref.WeakReference
+
+internal class StateRouter(
+ private val fragmentManager: WeakReference,
+) {
+ var currentState: MutableStateFlow = MutableStateFlow(SendUiStateType.Amount)
+
+ fun onBackClick() {
+ fragmentManager.get()?.popBackStack()
+ }
+
+ fun onNextClick() {
+ when (currentState.value) {
+ SendUiStateType.Amount -> {
+ currentState.update { SendUiStateType.Recipient }
+ }
+ SendUiStateType.Recipient -> {
+ currentState.update { SendUiStateType.Fee }
+ }
+ else -> {
+ // todo implement
+ }
+ }
+ }
+
+ fun onPrevClick() {
+ when (currentState.value) {
+ SendUiStateType.Amount -> onBackClick()
+ SendUiStateType.Recipient -> currentState.update { SendUiStateType.Amount }
+ SendUiStateType.Fee -> currentState.update { SendUiStateType.Recipient }
+ else -> onBackClick()
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt
index 3545aeccc6..346cd114aa 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt
@@ -1,64 +1,55 @@
package com.tangem.features.send.impl.presentation.state.amount
-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.state.SendUiState
+import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
-import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf
+import kotlinx.coroutines.flow.MutableStateFlow
internal class SendAmountStateConverter(
- private val currentStateProvider: Provider,
private val appCurrencyProvider: Provider,
- private val userWalletProvider: Provider,
- private val clickIntents: SendClickIntents,
+ private val userWalletProvider: Provider,
+ private val cryptoCurrencyStatusProvider: Provider,
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
private val sendAmountFieldConverter: SendAmountFieldConverter,
-) : Converter, SendUiState> {
+) : Converter {
- override fun convert(value: Either): SendUiState {
- val userWallet = userWalletProvider() ?: return currentStateProvider()
+ override fun convert(value: Unit): SendStates.AmountState {
+ val userWallet = userWalletProvider()
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,
- ),
- ),
- )
- },
+ val status = cryptoCurrencyStatusProvider()
+ val fiat = formatFiatAmount(status.value.fiatAmount, appCurrency.code, appCurrency.symbol)
+ val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals)
+
+ return SendStates.AmountState(
+ appCurrency = appCurrency,
+ cryptoCurrencyStatus = status,
+ walletName = userWallet.name,
+ walletBalance = "$crypto ($fiat)",
+ tokenIconState = iconStateConverter.convert(status),
+ amountTextField = MutableStateFlow(sendAmountFieldConverter.convert(Unit)),
+ isFiatValue = false,
+ isPrimaryButtonEnabled = false,
+ segmentedButtonConfig = persistentListOf(
+ SendAmountSegmentedButtonsConfig(
+ title = stringReference(status.currency.symbol),
+ iconState = iconStateConverter.convert(status),
+ isFiat = false,
+ ),
+ SendAmountSegmentedButtonsConfig(
+ title = stringReference(appCurrency.code),
+ iconState = iconStateConverter.convert(status),
+ isFiat = true,
+ ),
+ ),
)
}
}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt
index 5c91a7489e..cacb180097 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt
@@ -1,8 +1,11 @@
package com.tangem.features.send.impl.presentation.state.fields
import com.tangem.common.Provider
+import com.tangem.domain.tokens.model.CryptoCurrencyStatus
+import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.utils.converter.Converter
+import kotlinx.coroutines.flow.update
import java.text.DecimalFormatSymbols
import java.text.NumberFormat
@@ -11,21 +14,14 @@ internal class SendAmountFieldChangeConverter(
) : Converter {
override fun convert(value: String): SendUiState {
val state = currentStateProvider()
+ val amountState = state.amountState ?: return state
- if (
- state !is SendUiState.Content.AmountState ||
- value.checkDecimalSeparatorDuplicate()
- ) {
- return state
- }
-
+ if (value.checkDecimalSeparatorDuplicate()) return state
if (value.isEmpty()) return state.emptyState()
- val fiatRate = state.cryptoCurrencyStatus.value.fiatRate
-
+ val fiatRate = amountState.cryptoCurrencyStatus.value.fiatRate
val trimmedValue = value.trim()
-
- val cryptoValue = if (state.isFiatValue) {
+ val cryptoValue = if (amountState.isFiatValue) {
if (value.isNotBlank()) {
trimmedValue.toBigDecimal().divide(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty()
} else {
@@ -35,7 +31,7 @@ internal class SendAmountFieldChangeConverter(
trimmedValue
}
- val fiatValue = if (!state.isFiatValue) {
+ val fiatValue = if (!amountState.isFiatValue) {
if (value.isNotBlank()) {
trimmedValue.toBigDecimal().multiply(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty()
} else {
@@ -45,37 +41,48 @@ internal class SendAmountFieldChangeConverter(
trimmedValue
}
- val isExceedBalance = value.checkExceedBalance(state)
- return state.copy(
- amountTextField = state.amountTextField.copy(
+ val isExceedBalance = value.checkExceedBalance(amountState.cryptoCurrencyStatus, amountState)
+ amountState.amountTextField.update {
+ it.copy(
value = cryptoValue,
fiatValue = fiatValue,
isError = isExceedBalance,
+ )
+ }
+ return state.copy(
+ amountState = amountState.copy(
+ isPrimaryButtonEnabled = !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,
+ private fun SendUiState.emptyState(): SendUiState {
+ amountState?.amountTextField?.update {
+ it.copy(
+ value = if (!amountState.isFiatValue) "" else DEFAULT_VALUE,
+ fiatValue = if (amountState.isFiatValue) "" else DEFAULT_VALUE,
isError = false,
+ )
+ }
+ return copy(
+ amountState = amountState?.copy(
+ isPrimaryButtonEnabled = false,
),
- isPrimaryButtonEnabled = false,
)
}
private fun String.checkDecimalSeparatorDuplicate(): Boolean {
- val regex = "[\\.\\,]".toRegex()
+ val regex = TRIM_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
+ private fun String.checkExceedBalance(
+ cryptoCurrencyStatus: CryptoCurrencyStatus,
+ state: SendStates.AmountState,
+ ): Boolean {
+ val currencyStatus = cryptoCurrencyStatus.value
return if (state.isFiatValue) {
toBigDecimal() > currencyStatus.fiatAmount
} else {
@@ -88,10 +95,11 @@ internal class SendAmountFieldChangeConverter(
if (length > 1 && firstOrNull() == '0' && get(1).isDigit()) trimmedValue = drop(1)
val separatorChar = DecimalFormatSymbols.getInstance().decimalSeparator.toString()
- return trimmedValue.replace("[\\.\\,]".toRegex(), separatorChar)
+ return trimmedValue.replace(TRIM_REGEX.toRegex(), separatorChar)
}
companion object {
private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00)
+ private const val TRIM_REGEX = "[.,]"
}
}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt
index d6577f14c2..4b027a624d 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt
@@ -22,7 +22,6 @@ internal class SendAmountFieldConverter(
imeAction = ImeAction.Next,
keyboardType = KeyboardType.Number,
),
- label = TextReference.Str(""),
placeholder = TextReference.Str(DEFAULT_VALUE),
isError = false,
error = TextReference.Res(R.string.send_insufficient_funds),
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt
index f62f981b67..456359a16c 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt
@@ -16,20 +16,43 @@ internal sealed class SendTextField {
/** Keyboard options */
abstract val keyboardOptions: KeyboardOptions
- /** Label */
- abstract val label: TextReference
-
- /** Placeholder (hint) */
- abstract val placeholder: 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 placeholder: TextReference,
val fiatValue: String,
val isError: Boolean,
val error: TextReference,
) : SendTextField()
+
+ data class RecipientAddress(
+ override val value: String,
+ override val onValueChange: (String) -> Unit,
+ override val keyboardOptions: KeyboardOptions,
+ val placeholder: TextReference,
+ val label: TextReference,
+ val isError: Boolean = false,
+ val error: TextReference? = null,
+ ) : SendTextField()
+
+ data class RecipientMemo(
+ override val value: String,
+ override val onValueChange: (String) -> Unit,
+ override val keyboardOptions: KeyboardOptions,
+ val placeholder: TextReference,
+ val label: TextReference,
+ val isError: Boolean = false,
+ val error: TextReference? = null,
+ ) : SendTextField()
+
+ data class CustomFee(
+ override val value: String,
+ override val onValueChange: (String) -> Unit,
+ override val keyboardOptions: KeyboardOptions,
+ val label: TextReference? = null,
+ ) : SendTextField()
}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt
new file mode 100644
index 0000000000..75cd69aa6a
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt
@@ -0,0 +1,31 @@
+package com.tangem.features.send.impl.presentation.state.recipient
+
+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.state.fields.SendTextField
+import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
+import com.tangem.utils.converter.Converter
+import kotlinx.coroutines.flow.MutableStateFlow
+
+internal class SendRecipientAddressFieldConverter(
+ private val clickIntents: SendClickIntents,
+) : Converter> {
+
+ override fun convert(value: Unit): MutableStateFlow {
+ return MutableStateFlow(
+ SendTextField.RecipientAddress(
+ value = "",
+ onValueChange = clickIntents::onRecipientAddressValueChange,
+ keyboardOptions = KeyboardOptions(
+ imeAction = ImeAction.Next,
+ keyboardType = KeyboardType.Text,
+ ),
+ placeholder = TextReference.Res(R.string.send_enter_address_field),
+ label = TextReference.Res(R.string.send_recipient),
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt
new file mode 100644
index 0000000000..da8a97622d
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt
@@ -0,0 +1,113 @@
+package com.tangem.features.send.impl.presentation.state.recipient
+
+import androidx.paging.*
+import com.tangem.common.Provider
+import com.tangem.core.ui.extensions.TextReference
+import com.tangem.core.ui.extensions.wrappedList
+import com.tangem.core.ui.utils.DateTimeFormatters
+import com.tangem.core.ui.utils.toDateFormat
+import com.tangem.core.ui.utils.toTimeFormat
+import com.tangem.domain.tokens.model.CryptoCurrencyStatus
+import com.tangem.domain.txhistory.models.TxHistoryItem
+import com.tangem.features.send.impl.R
+import com.tangem.features.send.impl.presentation.domain.AvailableWallet
+import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
+import com.tangem.features.send.impl.presentation.state.SendUiState
+import com.tangem.utils.toFormattedCurrencyString
+import kotlinx.collections.immutable.toPersistentList
+import kotlinx.coroutines.flow.update
+
+internal class SendRecipientListConverter(
+ private val currentStateProvider: Provider,
+ private val cryptoCurrencyStatusProvider: Provider,
+) {
+
+ fun convert(wallets: List, txHistory: PagingData) {
+ val filteredWallets = wallets.filterNotNull()
+ .groupBy { item -> item.name }
+ .values.flatten()
+ .mapIndexed { index, item ->
+ item.copy(
+ name = "${item.name} ${index.inc()}",
+ )
+ }
+
+ currentStateProvider().recipientList.update {
+ txHistory.filter { item ->
+ val isTransfer = item.type == TxHistoryItem.TransactionType.Transfer
+ val isNotContract = item.interactionAddressType is TxHistoryItem.InteractionAddressType.User
+ val isSingleAddress = if (item.isOutgoing) {
+ item.destinationType is TxHistoryItem.DestinationType.Single
+ } else {
+ item.sourceType is TxHistoryItem.SourceType.Single
+ }
+ isTransfer && isSingleAddress && isNotContract
+ }.map { tx ->
+ SendRecipientListContent.Item(
+ id = tx.txHash,
+ title = tx.extractAddress(),
+ subtitle = TextReference.Str(tx.getAmount()),
+ info = tx.extractTimestamp(),
+ subtitleIconRes = tx.extractIconRes(),
+ )
+ }.insertWallets(filteredWallets)
+ }
+ }
+
+ private fun PagingData.insertWallets(
+ wallets: List,
+ ): PagingData {
+ return insertSeparators(terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE) { before, after ->
+ return@insertSeparators when {
+ before == null && after is SendRecipientListContent.Item -> {
+ SendRecipientListContent.Wallets(
+ wallets.map {
+ SendRecipientListContent.Item(
+ id = it.address,
+ title = TextReference.Str(it.address),
+ subtitle = TextReference.Str(it.name),
+ )
+ }.toPersistentList(),
+ )
+ }
+ else -> null
+ }
+ }
+ }
+
+ private fun TxHistoryItem.extractAddress(): TextReference = if (isOutgoing) {
+ when (val destination = destinationType) {
+ is TxHistoryItem.DestinationType.Multiple -> TextReference.Res(
+ R.string.transaction_history_multiple_addresses,
+ )
+ is TxHistoryItem.DestinationType.Single -> TextReference.Str(destination.addressType.address)
+ }
+ } else {
+ when (val source = sourceType) {
+ is TxHistoryItem.SourceType.Multiple -> TextReference.Res(R.string.transaction_history_multiple_addresses)
+ is TxHistoryItem.SourceType.Single -> TextReference.Str(source.address)
+ }
+ }
+
+ private fun TxHistoryItem.extractIconRes() = if (isOutgoing) {
+ R.drawable.ic_arrow_up_24
+ } else {
+ R.drawable.ic_arrow_down_24
+ }
+
+ private fun TxHistoryItem.getAmount(): String {
+ val cryptoCurrency = cryptoCurrencyStatusProvider().currency
+ return amount.toFormattedCurrencyString(
+ currency = cryptoCurrency.symbol,
+ decimals = cryptoCurrency.decimals,
+ )
+ }
+
+ private fun TxHistoryItem.extractTimestamp(): TextReference {
+ val date = timestampInMillis.toDateFormat(
+ formatter = DateTimeFormatters.dateDDMMYYYY,
+ )
+ val time = timestampInMillis.toTimeFormat()
+ return TextReference.Res(R.string.send_date_format, wrappedList(date, time))
+ }
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt
new file mode 100644
index 0000000000..d1890038cb
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt
@@ -0,0 +1,51 @@
+package com.tangem.features.send.impl.presentation.state.recipient
+
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.common.Provider
+import com.tangem.core.ui.extensions.TextReference
+import com.tangem.domain.tokens.model.CryptoCurrencyStatus
+import com.tangem.features.send.impl.R
+import com.tangem.features.send.impl.presentation.state.fields.SendTextField
+import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
+import com.tangem.utils.converter.Converter
+import kotlinx.coroutines.flow.MutableStateFlow
+
+internal class SendRecipientMemoFieldConverter(
+ private val clickIntents: SendClickIntents,
+ private val cryptoCurrencyStatus: Provider,
+) : Converter> {
+
+ fun convertOrNull(): MutableStateFlow? {
+ val cryptoCurrency = cryptoCurrencyStatus().currency
+
+ return when (cryptoCurrency.network.id.value) {
+ Blockchain.XRP.id -> convert(R.string.send_destination_tag_field)
+ Blockchain.Binance.id,
+ Blockchain.TON.id,
+ Blockchain.Cosmos.id,
+ Blockchain.TerraV1.id,
+ Blockchain.TerraV2.id,
+ Blockchain.Stellar.id,
+ -> convert(R.string.send_extras_hint_memo)
+ else -> null
+ }
+ }
+
+ override fun convert(value: Int): MutableStateFlow {
+ return MutableStateFlow(
+ SendTextField.RecipientMemo(
+ value = "",
+ onValueChange = clickIntents::onRecipientMemoValueChange,
+ keyboardOptions = KeyboardOptions(
+ imeAction = ImeAction.Done,
+ keyboardType = KeyboardType.Text,
+ ),
+ placeholder = TextReference.Res(R.string.send_optional_field),
+ label = TextReference.Res(value),
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt
new file mode 100644
index 0000000000..ebdafea549
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt
@@ -0,0 +1,30 @@
+package com.tangem.features.send.impl.presentation.state.recipient
+
+import com.tangem.common.Provider
+import com.tangem.domain.tokens.model.CryptoCurrencyStatus
+import com.tangem.features.send.impl.presentation.state.SendStates
+import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
+import com.tangem.utils.converter.Converter
+
+internal class SendRecipientStateConverter(
+ private val clickIntents: SendClickIntents,
+ private val cryptoCurrencyStatusProvider: Provider,
+) : Converter {
+
+ private val addressFieldConverter by lazy { SendRecipientAddressFieldConverter(clickIntents) }
+ private val memoFieldConverter by lazy {
+ SendRecipientMemoFieldConverter(
+ clickIntents,
+ cryptoCurrencyStatusProvider,
+ )
+ }
+
+ override fun convert(value: Unit): SendStates.RecipientState {
+ return SendStates.RecipientState(
+ addressTextField = addressFieldConverter.convert(Unit),
+ memoTextField = memoFieldConverter.convertOrNull(),
+ network = cryptoCurrencyStatusProvider().currency.network.name,
+ isPrimaryButtonEnabled = false,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt
index 599b46babc..30ca727485 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
@@ -19,9 +20,10 @@ 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.state.SendUiState
+import com.tangem.features.send.impl.presentation.state.SendUiStateType
@Composable
-internal fun SendNavigationButtons(uiState: SendUiState.Content) {
+internal fun SendNavigationButtons(uiState: SendUiState) {
Row(
modifier = Modifier
.fillMaxWidth()
@@ -38,9 +40,11 @@ internal fun SendNavigationButtons(uiState: SendUiState.Content) {
}
@Composable
-private fun SendSecondaryNavigationButton(uiState: SendUiState.Content) {
+private fun SendSecondaryNavigationButton(uiState: SendUiState) {
+ val currentState = uiState.currentState.collectAsState()
AnimatedVisibility(
- visible = uiState is SendUiState.Content.RecipientState || uiState is SendUiState.Content.FeeState,
+ visible = currentState.value == SendUiStateType.Recipient ||
+ currentState.value == SendUiStateType.Fee,
) {
Icon(
modifier = Modifier
@@ -48,46 +52,52 @@ private fun SendSecondaryNavigationButton(uiState: SendUiState.Content) {
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
.background(TangemTheme.colors.button.secondary)
.clickable {
- // todo add prev click
+ uiState.clickIntents.onPrevClick()
}
.padding(TangemTheme.dimens.spacing12),
painter = painterResource(R.drawable.ic_back_24),
+ tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
}
}
@Composable
-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,
+private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier = Modifier) {
+ val currentState = uiState.currentState.collectAsState()
+
+ val buttonTextId = when (currentState.value) {
+ SendUiStateType.Amount,
+ SendUiStateType.Recipient,
+ SendUiStateType.Fee,
-> R.string.common_next
- is SendUiState.Content.SendState -> R.string.common_send
+ SendUiStateType.Send -> R.string.common_send
else -> R.string.common_close
}
+
+ val isButtonEnabled = when (currentState.value) {
+ SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false
+ SendUiStateType.Recipient -> uiState.recipientState?.isPrimaryButtonEnabled ?: false
+ else -> true
+ }
+
AnimatedContent(
targetState = buttonTextId,
label = "Update send screen state",
modifier = modifier,
) { textId ->
- if (uiState is SendUiState.Content.SendState) {
+ if (currentState.value == SendUiStateType.Send) {
PrimaryButtonIconEnd(
text = stringResource(textId),
iconResId = R.drawable.ic_tangem_24,
- enabled = uiState.isPrimaryButtonEnabled,
- onClick = {
- // todo add next click
- },
+ enabled = isButtonEnabled,
+ onClick = uiState.clickIntents::onNextClick,
)
} else {
PrimaryButton(
text = stringResource(textId),
- enabled = uiState.isPrimaryButtonEnabled,
- onClick = {
- // todo add next click
- },
+ enabled = isButtonEnabled,
+ onClick = uiState.clickIntents::onNextClick,
)
}
}
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt
index 34df63ae5a..93a0f62d57 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt
@@ -1,53 +1,95 @@
package com.tangem.features.send.impl.presentation.ui
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.scrollable
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetDraggableHeader
+import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.paging.compose.collectAsLazyPagingItems
+import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
import com.tangem.core.ui.res.TangemTheme
+import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.SendUiState
+import com.tangem.features.send.impl.presentation.state.SendUiStateType
+import com.tangem.features.send.impl.presentation.ui.amount.SendAmountContent
+import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent
@Composable
-internal fun SendScreen(uiState: SendUiState.Content) {
+internal fun SendScreen(uiState: SendUiState) {
+ val currentState = uiState.currentState.collectAsStateWithLifecycle()
+ BackHandler { uiState.clickIntents.onPrevClick() }
Column(
modifier = Modifier
+ .fillMaxSize()
+ .systemBarsPadding()
.imePadding()
- .background(
- color = TangemTheme.colors.background.tertiary,
- shape = RoundedCornerShape(
- topStart = TangemTheme.dimens.radius24,
- topEnd = TangemTheme.dimens.radius24,
- ),
- ),
+ .background(color = TangemTheme.colors.background.tertiary),
horizontalAlignment = Alignment.CenterHorizontally,
) {
- TangemBottomSheetDraggableHeader(
- color = TangemTheme.colors.background.tertiary,
- )
- Box(
- modifier = Modifier
- .weight(1f)
- .scrollable(state = rememberScrollState(), orientation = Orientation.Vertical),
- ) {
- SendScreenContent(uiState)
+ val titleRes = when (currentState.value) {
+ SendUiStateType.Amount,
+ SendUiStateType.Send,
+ -> R.string.common_send
+ SendUiStateType.Recipient -> R.string.send_recipient
+ SendUiStateType.Fee -> R.string.common_fee_selector_title
+ SendUiStateType.Done -> null
}
+ val iconRes = when (currentState.value) {
+ SendUiStateType.Amount,
+ SendUiStateType.Recipient,
+ -> R.drawable.ic_qrcode_scan_24
+ else -> null
+ }
+
+ AppBarWithBackButtonAndIcon(
+ text = titleRes?.let { stringResource(it) },
+ onBackClick = uiState.clickIntents::onBackClick,
+ onIconClick = uiState.clickIntents::onQrCodeScanClick,
+ backIconRes = R.drawable.ic_close_24,
+ iconRes = iconRes,
+ backgroundColor = TangemTheme.colors.background.tertiary,
+ )
+ SendScreenContent(
+ uiState = uiState,
+ currentState = currentState,
+ modifier = Modifier
+ .weight(1f),
+ )
SendNavigationButtons(uiState)
}
}
@Composable
-private fun SendScreenContent(uiState: SendUiState.Content) {
- when (uiState) {
- is SendUiState.Content.AmountState -> SendAmountContent(uiState)
- else -> { /* [REDACTED_TODO_COMMENT]*/
+private fun SendScreenContent(
+ uiState: SendUiState,
+ currentState: State,
+ modifier: Modifier = Modifier,
+) {
+ val recipientList = uiState.recipientList.collectAsLazyPagingItems()
+ AnimatedContent(
+ targetState = currentState.value,
+ label = "Send Scree Navigation",
+ modifier = modifier,
+ ) { state ->
+ when (state) {
+ SendUiStateType.Amount -> SendAmountContent(
+ uiState.amountState,
+ uiState.clickIntents,
+ )
+ SendUiStateType.Recipient -> SendRecipientContent(
+ uiState.recipientState,
+ uiState.clickIntents,
+ recipientList,
+ )
+ 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/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt
index 5cd1c54f34..2fedacc5c1 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt
@@ -18,12 +18,8 @@ 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.components.fields.AmountVisualTransformation
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@@ -140,29 +136,4 @@ private fun AmountFieldError(isError: Boolean, error: TextReference, modifier: M
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/ui/amount/AmountFieldContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt
index 0bb6d13de0..363392865b 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt
@@ -11,12 +11,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.res.TangemTheme
-import com.tangem.features.send.impl.presentation.state.SendUiState
+import com.tangem.features.send.impl.presentation.state.SendStates
@Composable
-internal fun AmountFieldContainer(amountState: SendUiState.Content.AmountState, modifier: Modifier = Modifier) {
+internal fun AmountFieldContainer(amountState: SendStates.AmountState, modifier: Modifier = Modifier) {
+ val amountTextField = amountState.amountTextField.collectAsStateWithLifecycle()
Column(
modifier = modifier
.fillMaxWidth()
@@ -52,10 +54,10 @@ internal fun AmountFieldContainer(amountState: SendUiState.Content.AmountState,
.align(Alignment.CenterHorizontally),
)
AmountField(
- sendField = amountState.amountTextField,
+ sendField = amountTextField.value,
+ isFiat = amountState.isFiatValue,
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/ui/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt
similarity index 75%
rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendAmountContent.kt
rename to features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt
index 0cc832db0c..d400239936 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendAmountContent.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt
@@ -1,4 +1,4 @@
-package com.tangem.features.send.impl.presentation.ui
+package com.tangem.features.send.impl.presentation.ui.amount
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@@ -6,10 +6,8 @@ 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
@@ -18,29 +16,20 @@ 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.state.SendStates
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig
-import com.tangem.features.send.impl.presentation.state.SendUiState
-import com.tangem.features.send.impl.presentation.ui.amount.AmountFieldContainer
+import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
@Composable
-internal fun SendAmountContent(amountState: SendUiState.Content.AmountState) {
+internal fun SendAmountContent(amountState: SendStates.AmountState?, clickIntents: SendClickIntents) {
+ if (amountState == null) return
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,
@@ -49,16 +38,16 @@ internal fun SendAmountContent(amountState: SendUiState.Content.AmountState) {
) {
SegmentedButtons(
modifier = Modifier
- .height(TangemTheme.dimens.size40)
- .weight(1f),
+ .weight(1f)
+ .height(TangemTheme.dimens.size40),
config = amountState.segmentedButtonConfig,
- onClick = { amountState.clickIntents.onCurrencyChangeClick(it.isFiat) },
+ onClick = { clickIntents.onCurrencyChangeClick(it.isFiat) },
) {
SendAmountCurrencyButton(it)
}
SecondaryButton(
text = stringResource(R.string.send_max_amount),
- onClick = amountState.clickIntents::onMaxValueClick,
+ onClick = clickIntents::onMaxValueClick,
size = TangemButtonSize.Text,
shape = RoundedCornerShape(TangemTheme.dimens.radius26),
modifier = Modifier
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt
new file mode 100644
index 0000000000..ce0fbcee3b
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt
@@ -0,0 +1,38 @@
+package com.tangem.features.send.impl.presentation.ui.common
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import com.tangem.core.ui.res.TangemTheme
+
+/**
+ * Container for footer info below the text field
+ *
+ * @param modifier of component
+ * @param footer text
+ * @param footerTopPadding padding between footer and field
+ * @param content field content
+ */
+@Composable
+internal fun FooterContainer(
+ modifier: Modifier = Modifier,
+ footer: String? = null,
+ footerTopPadding: Dp = TangemTheme.dimens.spacing8,
+ content: @Composable () -> Unit,
+) {
+ Column(modifier = modifier) {
+ content()
+ footer?.let {
+ Text(
+ text = it,
+ style = TangemTheme.typography.caption2,
+ color = TangemTheme.colors.text.tertiary,
+ modifier = Modifier
+ .padding(top = footerTopPadding),
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt
index 2c3bfff4b4..f7c2cd613f 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt
@@ -13,9 +13,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
+import androidx.constraintlayout.compose.ConstraintLayout
+import androidx.constraintlayout.compose.Dimension
+import androidx.constraintlayout.compose.Visibility
import com.tangem.core.ui.components.MiddleEllipsisText
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.res.TangemTheme
@@ -28,75 +32,98 @@ import com.tangem.features.send.impl.R
* @param subtitle subtitle
* @param onClick click listener
* @param modifier modifier
+ * @param info info
* @param subtitleIconRes icon
*/
+@Suppress("DestructuringDeclarationWithTooManyEntries", "LongMethod")
@Composable
fun ListItemWithIcon(
title: String,
subtitle: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
+ info: String? = null,
@DrawableRes subtitleIconRes: Int? = null,
) {
- Row(
+ ConstraintLayout(
modifier = modifier
.fillMaxWidth()
- .background(TangemTheme.colors.background.action)
.clickable { onClick() }
- .padding(
- vertical = TangemTheme.dimens.spacing8,
- horizontal = TangemTheme.dimens.spacing12,
- ),
+ .padding(horizontal = TangemTheme.dimens.spacing12),
) {
+ val (iconRef, titleRef, subtitleRef, subtitleIconRef, infoRef) = createRefs()
+
+ val spacing2 = TangemTheme.dimens.spacing2
+ val spacing8 = TangemTheme.dimens.spacing8
+ val spacing10 = TangemTheme.dimens.spacing10
+ val spacing12 = TangemTheme.dimens.spacing12
IdentIcon(
address = title,
modifier = Modifier
.size(TangemTheme.dimens.size40)
- .clip(RoundedCornerShape(TangemTheme.dimens.radius20)),
+ .clip(RoundedCornerShape(TangemTheme.dimens.radius20))
+ .constrainAs(iconRef) {
+ start.linkTo(parent.start)
+ top.linkTo(parent.top, margin = spacing8)
+ bottom.linkTo(parent.bottom, margin = spacing8)
+ },
)
- Column(
+ MiddleEllipsisText(
+ text = title,
+ style = TangemTheme.typography.subtitle2,
+ color = TangemTheme.colors.text.primary1,
+ textAlign = TextAlign.Justify,
modifier = Modifier
- .fillMaxWidth()
- .padding(
- start = TangemTheme.dimens.spacing12,
- top = TangemTheme.dimens.spacing2,
- bottom = TangemTheme.dimens.spacing2,
- ),
- ) {
- MiddleEllipsisText(
- text = title,
- style = TangemTheme.typography.subtitle2,
- color = TangemTheme.colors.text.primary1,
- textAlign = TextAlign.Justify,
- modifier = Modifier.fillMaxSize(),
+ .constrainAs(titleRef) {
+ start.linkTo(iconRef.end, margin = spacing12)
+ end.linkTo(parent.end)
+ top.linkTo(parent.top, margin = spacing10)
+ width = Dimension.fillToConstraints
+ },
+ )
+ Icon(
+ painter = painterResource(id = subtitleIconRes ?: R.drawable.ic_arrow_down_24),
+ contentDescription = null,
+ tint = TangemTheme.colors.icon.informative,
+ modifier = Modifier
+ .size(TangemTheme.dimens.size16)
+ .background(TangemTheme.colors.background.tertiary, CircleShape)
+ .constrainAs(subtitleIconRef) {
+ start.linkTo(iconRef.end, margin = spacing12)
+ top.linkTo(titleRef.bottom)
+ bottom.linkTo(parent.bottom, margin = spacing10)
+ visibility = if (subtitleIconRes == null) Visibility.Gone else Visibility.Visible
+ },
+ )
+ Text(
+ text = subtitle,
+ style = TangemTheme.typography.caption2,
+ color = TangemTheme.colors.text.tertiary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier
+ .constrainAs(subtitleRef) {
+ start.linkTo(subtitleIconRef.end, margin = spacing2, goneMargin = spacing12)
+ end.linkTo(infoRef.start)
+ top.linkTo(titleRef.bottom)
+ bottom.linkTo(parent.bottom, margin = spacing10)
+ width = Dimension.fillToConstraints
+ },
+ )
+ info?.let {
+ Text(
+ text = it,
+ style = TangemTheme.typography.caption2,
+ color = TangemTheme.colors.text.tertiary,
+ maxLines = 1,
+ modifier = Modifier
+ .constrainAs(infoRef) {
+ start.linkTo(subtitleRef.end, goneMargin = spacing12)
+ end.linkTo(parent.end)
+ top.linkTo(titleRef.bottom)
+ bottom.linkTo(parent.bottom, margin = spacing10)
+ },
)
- Row {
- subtitleIconRes?.let { iconRes ->
- Icon(
- painter = painterResource(id = iconRes),
- contentDescription = null,
- tint = TangemTheme.colors.icon.informative,
- modifier = Modifier
- .size(TangemTheme.dimens.size16)
- .background(TangemTheme.colors.background.tertiary, CircleShape)
- .padding(TangemTheme.dimens.spacing3),
- )
- }
- Text(
- text = subtitle,
- style = TangemTheme.typography.caption2,
- color = TangemTheme.colors.text.tertiary,
- maxLines = 1,
- modifier = Modifier
- .then(
- if (subtitleIconRes != null) {
- Modifier.padding(start = TangemTheme.dimens.spacing4)
- } else {
- Modifier
- },
- ),
- )
- }
}
}
}
@@ -111,6 +138,7 @@ private fun ListItemWithIconPreview_Light(
ListItemWithIcon(
title = config.title,
subtitle = config.subtitle,
+ info = config.info,
subtitleIconRes = config.iconRes,
onClick = {},
)
@@ -126,6 +154,7 @@ private fun ListItemWithIconPreview_Dark(
ListItemWithIcon(
title = config.title,
subtitle = config.subtitle,
+ info = config.info,
subtitleIconRes = config.iconRes,
onClick = {},
)
@@ -135,6 +164,7 @@ private fun ListItemWithIconPreview_Dark(
private data class ListItemWithIconPreviewConfig(
val title: String,
val subtitle: String,
+ val info: String? = null,
val iconRes: Int? = null,
)
@@ -142,7 +172,14 @@ private class ListItemWithIconPreviewProvider : CollectionPreviewParameterProvid
collection = listOf(
ListItemWithIconPreviewConfig(
title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
- subtitle = "Wallet",
+ subtitle = "0.000000000000000000000000000000 BTC",
+ info = "0.0.0000 at 00:00",
+ iconRes = R.drawable.ic_arrow_down_24,
+ ),
+ ListItemWithIconPreviewConfig(
+ title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
+ subtitle = "1 BTC",
+ info = "0.0.0000 at 00:00",
iconRes = R.drawable.ic_arrow_down_24,
),
ListItemWithIconPreviewConfig(
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt
new file mode 100644
index 0000000000..ada2667527
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt
@@ -0,0 +1,199 @@
+package com.tangem.features.send.impl.presentation.ui.recipient
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.stringResource
+import androidx.paging.compose.LazyPagingItems
+import androidx.paging.compose.itemContentType
+import androidx.paging.compose.itemKey
+import com.tangem.core.ui.extensions.resolveReference
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.features.send.impl.R
+import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
+import com.tangem.features.send.impl.presentation.state.SendStates
+import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
+
+private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY"
+private const val MEMO_FIELD_KEY = "MEMO_FIELD_KEY"
+private const val MY_WALLETS_HEADER_KEY = "MY_WALLETS_HEADER_KEY"
+
+@Composable
+internal fun SendRecipientContent(
+ uiState: SendStates.RecipientState?,
+ clickIntents: SendClickIntents,
+ recipientList: LazyPagingItems,
+) {
+ if (uiState == null) return
+ val address = uiState.addressTextField.collectAsState().value
+ val memo = uiState.memoTextField?.collectAsState()?.value
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(TangemTheme.colors.background.tertiary)
+ .padding(horizontal = TangemTheme.dimens.spacing16),
+ ) {
+ item(key = ADDRESS_FIELD_KEY) {
+ TextFieldWithPasteAndIcon(
+ value = address.value,
+ label = address.label,
+ placeholder = address.placeholder,
+ footer = stringResource(R.string.send_recipient_address_footer, uiState.network),
+ onValueChange = address.onValueChange,
+ onPasteClick = clickIntents::onRecipientAddressValueChange,
+ singleLine = true,
+ modifier = Modifier.padding(top = TangemTheme.dimens.spacing4),
+ isError = address.isError,
+ error = address.error,
+ )
+ }
+ memo?.let { memoField ->
+ item(key = MEMO_FIELD_KEY) {
+ TextFieldWithPaste(
+ value = memoField.value,
+ label = memoField.label,
+ placeholder = memoField.placeholder,
+ footer = stringResource(R.string.send_recipient_memo_footer),
+ onValueChange = memoField.onValueChange,
+ onPasteClick = clickIntents::onRecipientMemoValueChange,
+ modifier = Modifier.padding(top = TangemTheme.dimens.spacing20),
+ isError = memoField.isError,
+ error = memoField.error,
+ )
+ }
+ }
+ recipientListItem(
+ recipientList = recipientList,
+ clickIntents = clickIntents,
+ )
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun LazyListScope.recipientListItem(
+ recipientList: LazyPagingItems,
+ clickIntents: SendClickIntents,
+) {
+ items(
+ count = recipientList.itemCount,
+ key = recipientList.itemKey {
+ when (it) {
+ is SendRecipientListContent.Wallets -> MY_WALLETS_HEADER_KEY
+ is SendRecipientListContent.Item -> it.id
+ }
+ },
+ contentType = recipientList.itemContentType { it::class.java },
+ ) { index ->
+ recipientList[index]?.let { item ->
+ when (item) {
+ is SendRecipientListContent.Wallets -> {
+ RecipientWalletListItem(
+ item = item,
+ clickIntents = clickIntents,
+ modifier = Modifier
+ .animateItemPlacement()
+ .padding(top = TangemTheme.dimens.spacing20)
+ .then(
+ if (index == 0) {
+ Modifier.clip(
+ RoundedCornerShape(
+ topEnd = TangemTheme.dimens.radius12,
+ topStart = TangemTheme.dimens.radius12,
+ ),
+ )
+ } else {
+ Modifier
+ },
+ ),
+ )
+ }
+ is SendRecipientListContent.Item -> {
+ val title = item.title.resolveReference()
+ ListItemWithIcon(
+ title = item.title.resolveReference(),
+ subtitle = item.subtitle.resolveReference(),
+ info = item.info?.let { ", ${it.resolveReference()}" },
+ subtitleIconRes = item.subtitleIconRes,
+ modifier = Modifier
+ .then(
+ if (index == recipientList.itemCount - 1) {
+ Modifier
+ .padding(bottom = TangemTheme.dimens.spacing20)
+ .clip(
+ RoundedCornerShape(
+ bottomEnd = TangemTheme.dimens.radius12,
+ bottomStart = TangemTheme.dimens.radius12,
+ ),
+ )
+ } else {
+ Modifier
+ },
+ )
+ .background(TangemTheme.colors.background.action),
+ onClick = { clickIntents.onRecipientAddressValueChange(title) },
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun RecipientWalletListItem(
+ item: SendRecipientListContent.Wallets,
+ clickIntents: SendClickIntents,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier
+ .background(TangemTheme.colors.background.action)
+ .padding(top = TangemTheme.dimens.spacing12),
+ ) {
+ if (item.list.isNotEmpty()) {
+ Text(
+ text = stringResource(R.string.send_recipient_wallets_title),
+ style = TangemTheme.typography.subtitle2,
+ color = TangemTheme.colors.text.tertiary,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ start = TangemTheme.dimens.spacing12,
+ end = TangemTheme.dimens.spacing12,
+ bottom = TangemTheme.dimens.spacing8,
+ ),
+ )
+ }
+ item.list.forEachIndexed { _, wallet ->
+ val title = wallet.title.resolveReference()
+ ListItemWithIcon(
+ title = wallet.title.resolveReference(),
+ subtitle = wallet.subtitle.resolveReference(),
+ onClick = { clickIntents.onRecipientAddressValueChange(title) },
+ )
+ }
+ Text(
+ text = stringResource(R.string.send_recent_transactions),
+ style = TangemTheme.typography.subtitle2,
+ color = TangemTheme.colors.text.tertiary,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ top = TangemTheme.dimens.spacing8,
+ bottom = TangemTheme.dimens.spacing8,
+ start = TangemTheme.dimens.spacing12,
+ end = TangemTheme.dimens.spacing12,
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt
index 1b9118851a..3a2ed57e1a 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt
@@ -23,14 +23,15 @@ import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.R
+import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
@Composable
internal fun TextFieldWithPasteAndIcon(
@@ -42,7 +43,14 @@ internal fun TextFieldWithPasteAndIcon(
modifier: Modifier = Modifier,
footer: String? = null,
singleLine: Boolean = false,
+ error: TextReference? = null,
+ isError: Boolean = false,
) {
+ val (title, color) = if (isError && error != null) {
+ error to TangemTheme.colors.text.warning
+ } else {
+ label to TangemTheme.colors.text.secondary
+ }
FooterContainer(modifier, footer) {
Column(
modifier = Modifier
@@ -53,9 +61,9 @@ internal fun TextFieldWithPasteAndIcon(
),
) {
Text(
- text = label.resolveReference(),
+ text = title.resolveReference(),
style = TangemTheme.typography.body2,
- color = TangemTheme.colors.text.secondary,
+ color = color,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing12,
@@ -114,7 +122,14 @@ internal fun TextFieldWithPaste(
onPasteClick: (String) -> Unit,
modifier: Modifier = Modifier,
footer: String? = null,
+ error: TextReference? = null,
+ isError: Boolean = false,
) {
+ val (title, color) = if (isError && error != null) {
+ error to TangemTheme.colors.text.warning
+ } else {
+ label to TangemTheme.colors.text.secondary
+ }
FooterContainer(modifier, footer) {
Row(
modifier = Modifier
@@ -129,9 +144,9 @@ internal fun TextFieldWithPaste(
.padding(TangemTheme.dimens.spacing12),
) {
Text(
- text = label.resolveReference(),
+ text = title.resolveReference(),
style = TangemTheme.typography.body2,
- color = TangemTheme.colors.text.secondary,
+ color = color,
)
SimpleTextField(
value = value,
@@ -160,6 +175,7 @@ internal fun TextFieldWithInfo(
modifier: Modifier = Modifier,
info: TextReference? = null,
footer: String? = null,
+ visualTransformation: VisualTransformation = VisualTransformation.None,
) {
FooterContainer(
footer = footer,
@@ -189,6 +205,7 @@ internal fun TextFieldWithInfo(
SimpleTextField(
value = value,
onValueChange = onValueChange,
+ visualTransformation = visualTransformation,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing6)
.weight(1f),
@@ -208,27 +225,6 @@ internal fun TextFieldWithInfo(
}
}
-@Composable
-private fun FooterContainer(
- modifier: Modifier = Modifier,
- footer: String? = null,
- footerTopPadding: Dp = TangemTheme.dimens.spacing8,
- content: @Composable () -> Unit,
-) {
- Column(modifier = modifier) {
- content()
- footer?.let {
- Text(
- text = it,
- style = TangemTheme.typography.caption2,
- color = TangemTheme.colors.text.tertiary,
- modifier = Modifier
- .padding(top = footerTopPadding),
- )
- }
- }
-}
-
@Composable
private fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) {
val clipboardManager = LocalClipboardManager.current
@@ -287,6 +283,7 @@ private fun SimpleTextField(
modifier: Modifier = Modifier,
placeholder: TextReference? = null,
singleLine: Boolean = false,
+ visualTransformation: VisualTransformation = VisualTransformation.None,
) {
val focusRequester = remember { FocusRequester() }
BasicTextField(
@@ -295,6 +292,7 @@ private fun SimpleTextField(
textStyle = TangemTheme.typography.body2,
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
singleLine = singleLine,
+ visualTransformation = visualTransformation,
decorationBox = { textValue ->
Box {
if (value.isBlank() && placeholder != null) {
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/AddressVerification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/AddressVerification.kt
new file mode 100644
index 0000000000..3dd89596a1
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/AddressVerification.kt
@@ -0,0 +1,18 @@
+package com.tangem.features.send.impl.presentation.viewmodel
+
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.blockchain.common.address.Address
+import com.tangem.domain.tokens.model.CryptoCurrency
+
+internal fun verifyAddress(address: String, cryptoCurrency: CryptoCurrency?): Boolean {
+ if (address.isEmpty()) return true
+ val blockchain = cryptoCurrency?.let {
+ Blockchain.fromId(cryptoCurrency.id.rawNetworkId)
+ } ?: return false
+
+ return blockchain.validateAddress(address)
+}
+
+internal fun isNotAddressInWallet(walletAddresses: Set, address: String): Boolean {
+ return walletAddresses.all { it.value != address }
+}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt
new file mode 100644
index 0000000000..a27ce13f23
--- /dev/null
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt
@@ -0,0 +1,46 @@
+package com.tangem.features.send.impl.presentation.viewmodel
+
+import androidx.core.text.isDigitsOnly
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.domain.tokens.model.CryptoCurrency
+import java.math.BigInteger
+
+internal fun validateMemo(memo: String, cryptoCurrency: CryptoCurrency?): Boolean {
+ if (cryptoCurrency == null) return false
+ return when (cryptoCurrency.network.id.value) {
+ Blockchain.XRP.id -> {
+ val tag = memo.toLongOrNull()
+ tag != null && tag <= XRP_TAG_MAX_NUMBER
+ }
+ Blockchain.Stellar.id -> {
+ isAssignableValue(memo)
+ }
+ else -> true
+ }
+}
+
+private fun isAssignableValue(value: String): Boolean {
+ val memoType = when {
+ value.isNotEmpty() && value.isDigitsOnly() -> XlmMemoType.ID
+ else -> XlmMemoType.TEXT
+ }
+ return when (memoType) {
+ XlmMemoType.TEXT -> {
+ // from org.stellar.sdk.MemoText
+ value.toByteArray().size <= XLM_MEMO_MAX_LENGTH
+ }
+ XlmMemoType.ID -> {
+ try {
+ // from com.tangem.blockchain.blockchains.stellar.StellarMemo.toStellarSdkMemo
+ value.toBigInteger() in BigInteger.ZERO..Long.MAX_VALUE.toBigInteger() * 2.toBigInteger()
+ } catch (ex: NumberFormatException) {
+ false
+ }
+ }
+ }
+}
+
+private enum class XlmMemoType { TEXT, ID }
+
+private const val XRP_TAG_MAX_NUMBER = 4294967295
+private const val XLM_MEMO_MAX_LENGTH = 28
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt
index 118d166c69..03740cb51e 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt
@@ -2,13 +2,25 @@ package com.tangem.features.send.impl.presentation.viewmodel
interface SendClickIntents {
+ fun onBackClick()
+
fun onNextClick()
fun onPrevClick()
+ fun onQrCodeScanClick()
+
+ // region Amount
fun onAmountValueChange(value: String)
fun onCurrencyChangeClick(isFiat: Boolean)
fun onMaxValueClick()
+ // endregion
+
+ // region Recipient
+ fun onRecipientAddressValueChange(value: String)
+
+ fun onRecipientMemoValueChange(value: String)
+ // endregion
}
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt
index 274feef98e..2bc9bb638a 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt
@@ -4,32 +4,53 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.*
+import androidx.paging.PagingData
import arrow.core.getOrElse
+import com.tangem.blockchain.blockchains.xrp.XrpAddressService
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.blockchain.common.address.Address
import com.tangem.common.Provider
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
+import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
+import com.tangem.domain.tokens.model.CryptoCurrencyStatus
+import com.tangem.domain.txhistory.models.TxHistoryItem
+import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
+import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
+import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.send.api.navigation.SendRouter
+import com.tangem.features.send.impl.presentation.domain.AvailableWallet
import com.tangem.features.send.impl.presentation.state.SendStateFactory
import com.tangem.features.send.impl.presentation.state.SendUiState
+import com.tangem.features.send.impl.presentation.state.StateRouter
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.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
+import kotlin.properties.Delegates
+@Suppress("LongParameterList")
@HiltViewModel
internal class SendViewModel @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
+ private val getWalletsUseCase: GetWalletsUseCase,
+ private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
+ private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
+ private val walletManagersFacade: WalletManagersFacade,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, SendClickIntents {
@@ -42,26 +63,41 @@ internal class SendViewModel @Inject constructor(
private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow()
+ private var innerRouter: StateRouter by Delegates.notNull()
+
private val stateFactory = SendStateFactory(
clickIntents = this,
currentStateProvider = Provider { uiState },
userWalletProvider = Provider { userWallet },
+ walletAddressesProvider = Provider { walletAddresses },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
+ cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
)
var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState())
private set
- private var userWallet: UserWallet? = null
+ private var userWallet: UserWallet by Delegates.notNull()
+ private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
+ private var walletAddresses = emptySet()
private var balanceJobHolder = JobHolder()
+ private var recipientsJobHolder = JobHolder()
+ private var walletAddressesJobHolder = JobHolder()
override fun onCreate(owner: LifecycleOwner) {
+ getWalletAddresses()
subscribeOnCurrencyStatusUpdates(owner)
+ getWalletsAndRecent()
+ }
+
+ fun setRouter(router: StateRouter) {
+ innerRouter = router
+ uiState = uiState.copy(currentState = router.currentState)
}
private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) {
- viewModelScope.launch(dispatchers.io) {
+ viewModelScope.launch(dispatchers.main) {
getUserWalletUseCase(userWalletId).fold(
ifRight = { wallet ->
userWallet = wallet
@@ -86,11 +122,12 @@ internal class SendViewModel @Inject constructor(
.conflate()
.distinctUntilChanged()
.onEach { either ->
- uiState = stateFactory.getAmountState(
- cryptoCurrencyStatus = either,
- )
+ either.onRight {
+ cryptoCurrencyStatus = it
+ uiState = stateFactory.getReadyState()
+ }
}
- .flowOn(dispatchers.io)
+ .flowOn(dispatchers.main)
.launchIn(viewModelScope)
.saveIn(balanceJobHolder)
}
@@ -107,27 +144,81 @@ internal class SendViewModel @Inject constructor(
)
}
- // region screen state navigation
- override fun onNextClick() {
- when (uiState) {
- is SendUiState.Content.AmountState -> onRecipientStateClick()
- is SendUiState.Content.RecipientState -> onFeeStateClick()
- else -> {
- // todo implement
+ private fun getWalletsAndRecent() {
+ combine(
+ flow = getUserWallets().conflate(),
+ flow2 = getTxHistory().conflate(),
+ ) { wallets, txHistory ->
+ stateFactory.onLoadedRecipientList(
+ wallets = wallets,
+ txHistory = txHistory,
+ )
+ }
+ .flowOn(dispatchers.io)
+ .launchIn(viewModelScope)
+ .saveIn(recipientsJobHolder)
+ }
+
+ private fun getUserWallets(): Flow
> {
+ return getWalletsUseCase()
+ .distinctUntilChanged()
+ .map { userWallets ->
+ coroutineScope {
+ userWallets
+ .filterNot { it.walletId == userWalletId || it.isLocked }
+ .map { wallet ->
+ async(dispatchers.io) {
+ getCryptoCurrenciesUseCase(wallet.walletId)
+ .fold(
+ ifRight = { currencyItem ->
+ val walletCurrency = currencyItem.firstOrNull {
+ it.network.id == cryptoCurrency.network.id
+ } ?: return@fold null
+ val addresses = walletManagersFacade.getAddress(
+ userWalletId = wallet.walletId,
+ network = walletCurrency.network,
+ )
+ return@fold AvailableWallet(
+ name = wallet.name,
+ address = addresses.first().value,
+ )
+ },
+ ifLeft = { null },
+ )
+ }
+ }
+ }.awaitAll()
}
+ }
+
+ private fun getTxHistory(): Flow> {
+ return flow {
+ txHistoryItemsUseCase(
+ userWalletId = userWalletId,
+ currency = cryptoCurrency,
+ ).fold(
+ ifRight = { emitAll(it.distinctUntilChanged()) },
+ ifLeft = {},
+ )
}
}
- override fun onPrevClick() {
- // todo implement
+ private fun getWalletAddresses() {
+ viewModelScope.launch(dispatchers.io) {
+ walletAddresses = walletManagersFacade.getAddresses(
+ userWalletId = userWalletId,
+ network = cryptoCurrency.network,
+ )
+ }.saveIn(walletAddressesJobHolder)
}
- private fun onRecipientStateClick() {
- stateFactory.getOnReceiveState()
- }
+ // region screen state navigation
+ override fun onBackClick() = innerRouter.onBackClick()
+ override fun onNextClick() = innerRouter.onNextClick()
+ override fun onPrevClick() = innerRouter.onPrevClick()
- private fun onFeeStateClick() {
- // todo implement
+ override fun onQrCodeScanClick() {
+ // TODO Add QR code scanning
}
// endregion
@@ -141,14 +232,44 @@ internal class SendViewModel @Inject constructor(
}
override fun onMaxValueClick() {
- val amountState = uiState as? SendUiState.Content.AmountState ?: return
-
+ val amountState = uiState.amountState ?: return
val amount = if (amountState.isFiatValue) {
amountState.cryptoCurrencyStatus.value.fiatAmount
} else {
amountState.cryptoCurrencyStatus.value.amount
}
- onAmountValueChange(amount?.toPlainString() ?: "0.00")
+ onAmountValueChange(amount?.toPlainString() ?: DEFAULT_VALUE)
}
// endregion
+
+ // region recipient state clicks
+ override fun onRecipientAddressValueChange(value: String) {
+ if (!checkIfXrpAddressValue(value)) {
+ uiState = stateFactory.getOnRecipientAddressValueChangeState(value)
+ }
+ }
+
+ override fun onRecipientMemoValueChange(value: String) {
+ if (!checkIfXrpAddressValue(value)) {
+ uiState = stateFactory.getOnRecipientMemoValueChangeState(value)
+ }
+ }
+
+ private fun checkIfXrpAddressValue(value: String): Boolean {
+ if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.first() == XRP_X_ADDRESS) {
+ viewModelScope.launch(dispatchers.io) {
+ val result = XrpAddressService.decodeXAddress(value)
+ onRecipientAddressValueChange(result?.address.orEmpty())
+ onRecipientMemoValueChange(result?.destinationTag.toString())
+ }
+ return true
+ }
+ return false
+ }
+ // endregion
+
+ companion object {
+ private const val XRP_X_ADDRESS = 'X'
+ private const val DEFAULT_VALUE = "0.00"
+ }
}
\ No newline at end of file
diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt
index 40261aefd2..819d32e1de 100644
--- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt
+++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryItemFlowConverter.kt
@@ -1,23 +1,19 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory
-import android.text.format.DateUtils
import androidx.paging.*
import com.tangem.common.Provider
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState
-import com.tangem.core.ui.utils.DateTimeFormatters
+import com.tangem.core.ui.utils.toDateFormat
+import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.converter.Converter
-import com.tangem.utils.extensions.isToday
-import com.tangem.utils.extensions.isYesterday
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
-import org.joda.time.DateTime
-import org.joda.time.DateTimeZone
internal class TokenDetailsTxHistoryItemFlowConverter(
private val currentStateProvider: Provider,
@@ -101,7 +97,7 @@ internal class TokenDetailsTxHistoryItemFlowConverter(
) {
val txContent = txHistoryItemState.state as TransactionState.Content
txHistoryItemState.copy(
- state = txContent.copy(timestamp = txContent.timestamp.toTimeFormat()),
+ state = txContent.copy(timestamp = txContent.timestamp.toLong().toTimeFormat()),
)
} else {
txHistoryItemState
@@ -117,26 +113,4 @@ internal class TokenDetailsTxHistoryItemFlowConverter(
null
}
}
-
- /**
- * If [this] timestamp is today or yesterday, returns relative date,
- * otherwise returns formatting date.
- */
- private fun Long.toDateFormat(): String {
- val localDate = DateTime(this, DateTimeZone.getDefault())
- return if (localDate.isToday() || localDate.isYesterday()) {
- DateUtils.getRelativeTimeSpanString(
- this,
- DateTime.now().millis,
- DateUtils.DAY_IN_MILLIS,
- DateUtils.FORMAT_ABBREV_RELATIVE,
- ).toString()
- } else {
- DateTimeFormatters.formatDate(date = localDate)
- }
- }
-
- private fun String.toTimeFormat(): String {
- return DateTimeFormatters.formatTime(time = DateTime(this.toLong(), DateTimeZone.getDefault()))
- }
}
\ No newline at end of file
diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml
index 6de5a3f3ab..86e6d89da1 100644
--- a/gradle/dependencies.toml
+++ b/gradle/dependencies.toml
@@ -34,7 +34,7 @@ compose-navigation = "2.7.4"
compose-accompanist = "0.30.1"
compose-paging = "3.2.1"
compose-reorderable = "0.9.6"
-compoese-lifecycle-runtime = "2.6.2"
+compose-lifecycle-runtime = "2.6.2"
# endregion Compose
# region Other libraries
@@ -142,7 +142,7 @@ androidx-palette = { module = "androidx.palette:palette", version.ref = "android
lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidxLifecycle" }
lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
lifecycle-viewModel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" }
-lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compoese-lifecycle-runtime" }
+lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compose-lifecycle-runtime" }
androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" }
# region AndroidX